Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
247 lines
11 KiB
Go
247 lines
11 KiB
Go
package model
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type MigrationIdentityFields struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Name string `gorm:"size:64;unique"`
|
|
Reference string `gorm:"size:64;uniqueIndex"`
|
|
Provider string `gorm:"size:32;uniqueIndex:,composite:provider_subject"`
|
|
Subject string `gorm:"size:64;uniqueIndex:,composite:provider_subject"`
|
|
}
|
|
|
|
type migrationIdentityV1 struct {
|
|
MigrationIdentityFields
|
|
Digest string `gorm:"type:char(32)"`
|
|
}
|
|
|
|
type migrationIdentityV2 struct {
|
|
MigrationIdentityFields
|
|
Digest string `gorm:"type:char(64)"`
|
|
Note string `gorm:"size:128"`
|
|
}
|
|
|
|
type migrationConstraintV1 struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Name string `gorm:"size:64"`
|
|
}
|
|
|
|
type migrationConstraintV2 struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Name string `gorm:"size:64;unique"`
|
|
}
|
|
|
|
type migrationDecimalV1 struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Price float64 `gorm:"type:decimal(10,6);default:0"`
|
|
}
|
|
|
|
type migrationDecimalV2 struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Price float64 `gorm:"type:decimal(12,6);not null;default:0"`
|
|
}
|
|
|
|
type migrationDecimalV3 struct {
|
|
ID int `gorm:"primaryKey"`
|
|
Price float64 `gorm:"type:decimal(12,6);not null;default:1.25"`
|
|
}
|
|
|
|
func TestMigrationSchemaStability(t *testing.T) {
|
|
for _, dialect := range []string{"sqlite", "mysql", "postgres"} {
|
|
t.Run(dialect, func(t *testing.T) {
|
|
var dsn string
|
|
switch dialect {
|
|
case "sqlite":
|
|
dsn = "local"
|
|
previousPath := common.SQLitePath
|
|
common.SQLitePath = filepath.Join(t.TempDir(), "migration.db")
|
|
t.Cleanup(func() { common.SQLitePath = previousPath })
|
|
case "mysql":
|
|
dsn = os.Getenv("TEST_MYSQL_DSN")
|
|
case "postgres":
|
|
dsn = os.Getenv("TEST_POSTGRES_DSN")
|
|
}
|
|
if dsn == "" {
|
|
t.Skip("test database DSN is not configured")
|
|
}
|
|
t.Setenv("MIGRATION_TEST_DSN", dsn)
|
|
db, _, err := chooseDB("MIGRATION_TEST_DSN", false)
|
|
require.NoError(t, err)
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
recorder := &migrationSQLRecorder{}
|
|
db = db.Session(&gorm.Session{Logger: recorder})
|
|
t.Run("request_log_correlation", func(t *testing.T) {
|
|
const table = "request_log_correlation_test"
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(table) })
|
|
tableDB := db.Table(table).Session(&gorm.Session{})
|
|
require.NoError(t, tableDB.AutoMigrate(&Log{}))
|
|
require.NoError(t, tableDB.AutoMigrate(&Log{}))
|
|
require.True(t, tableDB.Migrator().HasIndex(&Log{}, "idx_logs_correlation"))
|
|
require.NoError(t, tableDB.Migrator().DropIndex(&Log{}, "idx_logs_correlation"))
|
|
require.NoError(t, tableDB.Migrator().DropColumn(&Log{}, "correlation_key"))
|
|
require.NoError(t, tableDB.Omit("correlation_key").Create(&Log{Id: 1, UserId: 1, Quota: 123, Other: "{}"}).Error)
|
|
for i := 0; i < 2; i++ {
|
|
require.NoError(t, tableDB.AutoMigrate(&Log{}))
|
|
}
|
|
var old Log
|
|
require.NoError(t, tableDB.First(&old, 1).Error)
|
|
require.Equal(t, 123, old.Quota)
|
|
require.Empty(t, old.CorrelationKey)
|
|
for _, row := range []Log{{Id: 2, UserId: 1, CorrelationKey: "user1-session"}, {Id: 3, UserId: 2, CorrelationKey: "user2-session"}} {
|
|
require.NoError(t, tableDB.Create(&row).Error)
|
|
}
|
|
var found []Log
|
|
require.NoError(t, tableDB.Where("correlation_key = ?", "user1-session").Find(&found).Error)
|
|
require.Len(t, found, 1)
|
|
require.Equal(t, 1, found[0].UserId)
|
|
require.True(t, tableDB.Migrator().HasIndex(&Log{}, "idx_logs_request_id"))
|
|
})
|
|
|
|
t.Run("identity_and_indexes", func(t *testing.T) {
|
|
const table = "migration_identity_test"
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(table) })
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationIdentityV1{}))
|
|
row := migrationIdentityV1{
|
|
MigrationIdentityFields: MigrationIdentityFields{ID: 1, Name: "root", Reference: "token-reference", Provider: "oidc", Subject: "subject"},
|
|
Digest: "old-digest",
|
|
}
|
|
require.NoError(t, db.Table(table).Create(&row).Error)
|
|
recorder.reset()
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationIdentityV1{}))
|
|
assert.Empty(t, recorder.schemaMutations())
|
|
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationIdentityV2{}))
|
|
columns, err := db.Table(table).Migrator().ColumnTypes(&migrationIdentityV2{})
|
|
require.NoError(t, err)
|
|
for _, column := range columns {
|
|
if column.Name() == "digest" {
|
|
length, ok := column.Length()
|
|
require.True(t, ok)
|
|
assert.EqualValues(t, 64, length)
|
|
}
|
|
}
|
|
assert.True(t, db.Table(table).Migrator().HasColumn(&migrationIdentityV2{}, "note"))
|
|
recorder.reset()
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationIdentityV2{}))
|
|
assert.Empty(t, recorder.schemaMutations())
|
|
var saved migrationIdentityV2
|
|
require.NoError(t, db.Table(table).First(&saved, 1).Error)
|
|
assert.Equal(t, row.MigrationIdentityFields, saved.MigrationIdentityFields)
|
|
expectedDigest := row.Digest
|
|
if dialect == "postgres" {
|
|
expectedDigest += strings.Repeat(" ", 64-len(row.Digest))
|
|
}
|
|
assert.Equal(t, expectedDigest, saved.Digest)
|
|
for _, duplicate := range []migrationIdentityV2{
|
|
{MigrationIdentityFields: MigrationIdentityFields{Name: "root", Reference: "other-1", Provider: "other", Subject: "1"}},
|
|
{MigrationIdentityFields: MigrationIdentityFields{Name: "other-2", Reference: "token-reference", Provider: "other", Subject: "2"}},
|
|
{MigrationIdentityFields: MigrationIdentityFields{Name: "other-3", Reference: "other-3", Provider: "oidc", Subject: "subject"}},
|
|
} {
|
|
assert.Error(t, db.Table(table).Create(&duplicate).Error)
|
|
}
|
|
})
|
|
|
|
t.Run("unique_constraint_changes", func(t *testing.T) {
|
|
const table = "migration_constraint_test"
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(table) })
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationConstraintV1{}))
|
|
require.NoError(t, db.Table(table).Create(&migrationConstraintV1{Name: "existing"}).Error)
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationConstraintV2{}))
|
|
assert.Error(t, db.Table(table).Create(&migrationConstraintV2{Name: "existing"}).Error)
|
|
recorder.reset()
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationConstraintV2{}))
|
|
assert.Empty(t, recorder.schemaMutations())
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationConstraintV1{}))
|
|
require.NoError(t, db.Table(table).Create(&migrationConstraintV1{Name: "existing"}).Error)
|
|
})
|
|
|
|
if dialect == "postgres" {
|
|
t.Run("renamed_unique_constraints", func(t *testing.T) {
|
|
const table = "migration_renamed_unique"
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(table) })
|
|
tableDB := db.Table(table).Session(&gorm.Session{})
|
|
require.NoError(t, tableDB.AutoMigrate(&migrationConstraintV1{}))
|
|
original := migrationConstraintV1{Name: "existing"}
|
|
require.NoError(t, tableDB.Create(&original).Error)
|
|
for _, name := range []string{"models_model_name_key", `imported "model" name`} {
|
|
require.NoError(t, db.Exec("ALTER TABLE ? ADD CONSTRAINT ? UNIQUE (name)", clause.Table{Name: table}, clause.Column{Name: name}).Error)
|
|
}
|
|
require.NoError(t, db.Exec("ALTER TABLE ? ADD CONSTRAINT keep_composite UNIQUE (id, name)", clause.Table{Name: table}).Error)
|
|
require.NoError(t, db.Exec("CREATE INDEX keep_name_lookup ON ? (name)", clause.Table{Name: table}).Error)
|
|
// A unique field must keep the old names and still reject duplicates.
|
|
require.NoError(t, tableDB.AutoMigrate(&migrationConstraintV2{}))
|
|
require.Error(t, tableDB.Create(&migrationConstraintV2{Name: "existing"}).Error)
|
|
require.NoError(t, db.Exec("CREATE TABLE migration_unique_reference (name varchar(64) REFERENCES migration_renamed_unique(name))").Error)
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable("migration_unique_reference") })
|
|
require.NoError(t, db.Exec("INSERT INTO migration_unique_reference (name) VALUES (?)", "existing").Error)
|
|
require.Error(t, tableDB.AutoMigrate(&migrationConstraintV1{}), "dependent foreign keys must not be cascaded away")
|
|
for _, name := range []string{"models_model_name_key", `imported "model" name`} {
|
|
assert.True(t, tableDB.Migrator().HasConstraint(&migrationConstraintV1{}, name), "all old constraints must survive rollback")
|
|
}
|
|
var references int64
|
|
require.NoError(t, db.Table("migration_unique_reference").Count(&references).Error)
|
|
assert.EqualValues(t, 1, references)
|
|
require.NoError(t, db.Migrator().DropTable("migration_unique_reference"))
|
|
// Removing column uniqueness must resolve both actual constraint names.
|
|
require.NoError(t, tableDB.AutoMigrate(&migrationConstraintV1{}))
|
|
recorder.reset()
|
|
require.NoError(t, tableDB.AutoMigrate(&migrationConstraintV1{}))
|
|
assert.Empty(t, recorder.schemaMutations())
|
|
require.NoError(t, tableDB.Create(&migrationConstraintV1{Name: "existing"}).Error)
|
|
assert.True(t, tableDB.Migrator().HasConstraint(&migrationConstraintV1{}, "keep_composite"))
|
|
assert.True(t, tableDB.Migrator().HasIndex(&migrationConstraintV1{}, "keep_name_lookup"))
|
|
var rows []migrationConstraintV1
|
|
require.NoError(t, tableDB.Order("id").Find(&rows).Error)
|
|
require.Len(t, rows, 2)
|
|
assert.Equal(t, original, rows[0])
|
|
})
|
|
}
|
|
|
|
if dialect == "mysql" {
|
|
t.Run("decimal_default_and_real_changes", func(t *testing.T) {
|
|
const table = "migration_decimal_test"
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(table) })
|
|
require.NoError(t, db.Table(table).AutoMigrate(&migrationDecimalV1{}))
|
|
require.NoError(t, db.Table(table).Create(&migrationDecimalV1{ID: 1, Price: 12.345678}).Error)
|
|
for _, target := range []any{&migrationDecimalV1{}, &migrationDecimalV2{}, &migrationDecimalV3{}} {
|
|
require.NoError(t, db.Table(table).AutoMigrate(target))
|
|
recorder.reset()
|
|
require.NoError(t, db.Table(table).AutoMigrate(target))
|
|
assert.Empty(t, recorder.schemaMutations())
|
|
}
|
|
columns, err := db.Table(table).Migrator().ColumnTypes(&migrationDecimalV3{})
|
|
require.NoError(t, err)
|
|
for _, column := range columns {
|
|
if column.Name() == "price" {
|
|
precision, scale, ok := column.DecimalSize()
|
|
require.True(t, ok)
|
|
assert.EqualValues(t, 12, precision)
|
|
assert.EqualValues(t, 6, scale)
|
|
nullable, ok := column.Nullable()
|
|
require.True(t, ok)
|
|
assert.False(t, nullable)
|
|
}
|
|
}
|
|
require.NoError(t, db.Table(table).Create(&map[string]any{"id": 2}).Error)
|
|
var prices []float64
|
|
require.NoError(t, db.Table(table).Order("id").Pluck("price", &prices).Error)
|
|
assert.Equal(t, []float64{12.345678, 1.25}, prices)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|