mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-11 02:46:58 +08:00
Upgrade the SQLite GORM driver to correctly distinguish unique indexes from constraints. Normalize equivalent MySQL decimal defaults and PostgreSQL CHAR metadata during migration comparison while preserving real schema changes. Validation: 27 fresh-start and rc.26 upgrade scenarios using SQLite 3.50.4, MySQL 5.7.44, PostgreSQL 9.6.24, and ClickHouse 25.8.33.6; 15 upgraded databases passed uniqueness checks. Relational databases issue no DDL on unchanged restarts. Existing ClickHouse TTL synchronization remains unchanged.
178 lines
6.5 KiB
Go
178 lines
6.5 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"
|
|
)
|
|
|
|
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("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 == "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)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|