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.
351 lines
12 KiB
Go
351 lines
12 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func testPrefillGroupMigrationNonPostgreSQL(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
var version string
|
|
versionQuery := "SELECT version()"
|
|
if db.Dialector.Name() == "sqlite" {
|
|
versionQuery = "SELECT sqlite_version()"
|
|
}
|
|
require.NoError(t, db.Raw(versionQuery).Scan(&version).Error)
|
|
t.Logf("%s version: %s", db.Dialector.Name(), version)
|
|
tableName := fmt.Sprintf("prefill_group_migration_%d", time.Now().UnixNano())
|
|
t.Cleanup(func() { _ = db.Migrator().DropTable(tableName) })
|
|
|
|
tableDB := db.Table(tableName)
|
|
require.NoError(t, tableDB.AutoMigrate(&PrefillGroup{}))
|
|
require.NoError(t, tableDB.Create(&PrefillGroup{
|
|
Name: "preserved-name",
|
|
Type: "model",
|
|
Items: JSONValue(`["gpt-test"]`),
|
|
Description: "preserve me",
|
|
}).Error)
|
|
|
|
recorder := &migrationSQLRecorder{}
|
|
for pass := range 2 {
|
|
recorder.reset()
|
|
require.NoError(t, migratePrefillGroupUniqueness(db.Session(&gorm.Session{Logger: recorder})))
|
|
require.NoError(t, tableDB.Session(&gorm.Session{Logger: recorder}).AutoMigrate(&PrefillGroup{}))
|
|
if pass == 1 {
|
|
assert.Empty(t, recorder.schemaMutations(), "repeated startup must not change the schema")
|
|
}
|
|
}
|
|
|
|
var preserved PrefillGroup
|
|
require.NoError(t, tableDB.Where("name = ?", "preserved-name").First(&preserved).Error)
|
|
assert.Equal(t, "preserve me", preserved.Description)
|
|
assert.JSONEq(t, `["gpt-test"]`, string(preserved.Items))
|
|
assert.True(t, tableDB.Migrator().HasIndex(&PrefillGroup{}, prefillGroupNameIndex))
|
|
require.Error(t, tableDB.Create(&PrefillGroup{Name: preserved.Name, Type: "model"}).Error)
|
|
}
|
|
|
|
func TestMigratePrefillGroupUniquenessSQLite(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
testPrefillGroupMigrationNonPostgreSQL(t, db)
|
|
}
|
|
|
|
func TestMigratePrefillGroupUniquenessMySQL(t *testing.T) {
|
|
dsn := strings.TrimSpace(os.Getenv("TEST_MYSQL_DSN"))
|
|
if dsn == "" {
|
|
t.Skip("TEST_MYSQL_DSN is not configured")
|
|
}
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
|
|
testPrefillGroupMigrationNonPostgreSQL(t, db)
|
|
}
|
|
|
|
func TestMigratePrefillGroupUniquenessPostgreSQL(t *testing.T) {
|
|
dsn := strings.TrimSpace(os.Getenv("TEST_POSTGRES_DSN"))
|
|
if dsn == "" {
|
|
t.Skip("TEST_POSTGRES_DSN is not configured")
|
|
}
|
|
|
|
db, err := gorm.Open(postgres.New(postgres.Config{
|
|
DSN: dsn,
|
|
PreferSimpleProtocol: true,
|
|
}), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
|
|
|
|
var version string
|
|
require.NoError(t, db.Raw("SELECT version()").Scan(&version).Error)
|
|
t.Log(version)
|
|
|
|
type indexDefinition struct {
|
|
Name string `gorm:"column:indexname"`
|
|
Definition string `gorm:"column:indexdef"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
constraints []string
|
|
indexes []string
|
|
replacePartialIndex bool
|
|
withoutDeletedAt bool
|
|
prepareOld func(*testing.T, *gorm.DB)
|
|
wantError string
|
|
}{
|
|
{name: "fresh"},
|
|
{
|
|
name: "legacy_constraint",
|
|
constraints: []string{"idx_prefill_groups_name"},
|
|
},
|
|
{
|
|
name: "legacy_standalone_index",
|
|
indexes: []string{"idx_prefill_groups_name"},
|
|
replacePartialIndex: true,
|
|
},
|
|
{
|
|
name: "renamed_constraints_and_indexes",
|
|
constraints: []string{"idx_prefill_groups_name", "prefill_groups_name_key"},
|
|
indexes: []string{"idx_37606_uk_prefill_name", `custom "name" index`},
|
|
},
|
|
{
|
|
name: "imported_index_without_soft_delete_column",
|
|
indexes: []string{"idx_37606_uk_prefill_name"},
|
|
replacePartialIndex: true,
|
|
withoutDeletedAt: true,
|
|
},
|
|
{
|
|
name: "global_index_uses_target_name",
|
|
indexes: []string{prefillGroupNameIndex},
|
|
replacePartialIndex: true,
|
|
},
|
|
{
|
|
name: "global_constraint_uses_target_name",
|
|
constraints: []string{prefillGroupNameIndex},
|
|
replacePartialIndex: true,
|
|
},
|
|
{
|
|
name: "non_conflicting_indexes_are_preserved",
|
|
constraints: []string{"prefill_groups_name_key"},
|
|
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
|
t.Helper()
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE INDEX ? ON ? (?)",
|
|
clause.Column{Name: "keep_prefill_name"},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "name"},
|
|
).Error)
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE UNIQUE INDEX ? ON ? (?, ?)",
|
|
clause.Column{Name: "keep_prefill_name_deleted_at"},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "name"},
|
|
clause.Column{Name: "deleted_at"},
|
|
).Error)
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE UNIQUE INDEX ? ON ? (lower(?)) WHERE deleted_at IS NULL",
|
|
clause.Column{Name: "keep_prefill_lower_name"},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "name"},
|
|
).Error)
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE UNIQUE INDEX ? ON ? (?) WHERE deleted_at IS NOT NULL",
|
|
clause.Column{Name: "keep_prefill_deleted_name"},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "name"},
|
|
).Error)
|
|
},
|
|
},
|
|
{
|
|
name: "unexpected_target_definition_rolls_back",
|
|
constraints: []string{"prefill_groups_name_key"},
|
|
indexes: []string{"idx_37606_uk_prefill_name"},
|
|
replacePartialIndex: true,
|
|
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
|
t.Helper()
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE INDEX ? ON ? (?)",
|
|
clause.Column{Name: prefillGroupNameIndex},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "type"},
|
|
).Error)
|
|
},
|
|
wantError: "unexpected definition",
|
|
},
|
|
{
|
|
name: "target_name_on_other_table_rolls_back",
|
|
indexes: []string{"idx_37606_uk_prefill_name"},
|
|
replacePartialIndex: true,
|
|
withoutDeletedAt: true,
|
|
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
|
t.Helper()
|
|
require.NoError(t, tx.Exec("CREATE TABLE other_groups (name varchar(64))").Error)
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE INDEX ? ON other_groups (name)",
|
|
clause.Column{Name: prefillGroupNameIndex},
|
|
).Error)
|
|
},
|
|
wantError: "unexpected definition",
|
|
},
|
|
{
|
|
name: "foreign_key_dependency_rolls_back",
|
|
constraints: []string{"prefill_groups_name_key"},
|
|
replacePartialIndex: true,
|
|
withoutDeletedAt: true,
|
|
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
|
t.Helper()
|
|
require.NoError(t, tx.Exec("CREATE TABLE referenced_groups (name varchar(64) REFERENCES prefill_groups(name))").Error)
|
|
require.NoError(t, tx.Exec("INSERT INTO referenced_groups (name) VALUES (?)", "shared-name").Error)
|
|
},
|
|
wantError: "drop conflicting prefill group constraint",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
t.Cleanup(func() { _ = tx.Rollback().Error })
|
|
|
|
schemaName := fmt.Sprintf("prefill_group_migration_%d", time.Now().UnixNano())
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE SCHEMA ?",
|
|
clause.Table{Name: schemaName},
|
|
).Error)
|
|
require.NoError(t, tx.Exec(
|
|
"SET LOCAL search_path TO ?",
|
|
clause.Table{Name: schemaName},
|
|
).Error)
|
|
|
|
require.NoError(t, migratePrefillGroupUniqueness(tx))
|
|
require.NoError(t, tx.AutoMigrate(&PrefillGroup{}))
|
|
original := PrefillGroup{
|
|
Name: "shared-name",
|
|
Type: "model",
|
|
Items: JSONValue(`["gpt-test"]`),
|
|
Description: "preserve me",
|
|
}
|
|
require.NoError(t, tx.Create(&original).Error)
|
|
if test.replacePartialIndex {
|
|
require.NoError(t, tx.Migrator().DropIndex(&PrefillGroup{}, prefillGroupNameIndex))
|
|
}
|
|
if test.withoutDeletedAt {
|
|
require.NoError(t, tx.Migrator().DropColumn(&PrefillGroup{}, "DeletedAt"))
|
|
}
|
|
for _, constraintName := range test.constraints {
|
|
require.NoError(t, tx.Exec(
|
|
"ALTER TABLE ? ADD CONSTRAINT ? UNIQUE (?)",
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: constraintName},
|
|
clause.Column{Name: "name"},
|
|
).Error)
|
|
}
|
|
for _, indexName := range test.indexes {
|
|
require.NoError(t, tx.Exec(
|
|
"CREATE UNIQUE INDEX ? ON ? (?)",
|
|
clause.Column{Name: indexName},
|
|
clause.Table{Name: "prefill_groups"},
|
|
clause.Column{Name: "name"},
|
|
).Error)
|
|
}
|
|
if test.prepareOld != nil {
|
|
test.prepareOld(t, tx)
|
|
}
|
|
var oldIndexes []indexDefinition
|
|
require.NoError(t, tx.Raw(`
|
|
SELECT indexname, indexdef FROM pg_catalog.pg_indexes
|
|
WHERE schemaname = current_schema() AND tablename = 'prefill_groups'
|
|
ORDER BY indexname`).Scan(&oldIndexes).Error)
|
|
if test.wantError != "" {
|
|
err := migratePrefillGroupUniqueness(tx)
|
|
require.ErrorContains(t, err, test.wantError)
|
|
for _, constraintName := range test.constraints {
|
|
assert.True(t, tx.Migrator().HasConstraint(&PrefillGroup{}, constraintName))
|
|
}
|
|
var restoredIndexes []indexDefinition
|
|
require.NoError(t, tx.Raw(`
|
|
SELECT indexname, indexdef FROM pg_catalog.pg_indexes
|
|
WHERE schemaname = current_schema() AND tablename = 'prefill_groups'
|
|
ORDER BY indexname`).Scan(&restoredIndexes).Error)
|
|
assert.Equal(t, oldIndexes, restoredIndexes)
|
|
assert.Equal(t, !test.withoutDeletedAt, tx.Migrator().HasColumn(&PrefillGroup{}, "DeletedAt"))
|
|
var preserved PrefillGroup
|
|
require.NoError(t, tx.Unscoped().First(&preserved, original.Id).Error)
|
|
assert.Equal(t, original, preserved)
|
|
return
|
|
}
|
|
|
|
recorder := &migrationSQLRecorder{}
|
|
migrationDB := tx.Session(&gorm.Session{Logger: recorder})
|
|
for pass := range 2 {
|
|
recorder.reset()
|
|
require.NoError(t, migratePrefillGroupUniqueness(migrationDB))
|
|
require.NoError(t, migrationDB.AutoMigrate(&PrefillGroup{}))
|
|
if pass == 1 {
|
|
assert.Empty(t, recorder.schemaMutations(), "repeated startup must not change the schema")
|
|
}
|
|
}
|
|
for _, oldIndex := range oldIndexes {
|
|
if oldIndex.Name == prefillGroupNameIndex || slices.Contains(test.constraints, oldIndex.Name) || slices.Contains(test.indexes, oldIndex.Name) {
|
|
continue
|
|
}
|
|
var definition string
|
|
require.NoError(t, tx.Raw(`
|
|
SELECT indexdef FROM pg_catalog.pg_indexes
|
|
WHERE schemaname = current_schema() AND indexname = ?`, oldIndex.Name).Scan(&definition).Error)
|
|
assert.Equal(t, oldIndex.Definition, definition)
|
|
}
|
|
|
|
var preserved PrefillGroup
|
|
require.NoError(t, tx.First(&preserved, original.Id).Error)
|
|
assert.Equal(t, original, preserved)
|
|
|
|
var targetIndexDefinition string
|
|
require.NoError(t, tx.Raw(`
|
|
SELECT indexdef
|
|
FROM pg_catalog.pg_indexes
|
|
WHERE schemaname = current_schema()
|
|
AND tablename = 'prefill_groups'
|
|
AND indexname = ?`, prefillGroupNameIndex).Scan(&targetIndexDefinition).Error)
|
|
assert.Contains(t, strings.ToLower(targetIndexDefinition), "unique index")
|
|
assert.Contains(t, strings.ToLower(targetIndexDefinition), "where (deleted_at is null)")
|
|
|
|
duplicateError := tx.Transaction(func(duplicateTx *gorm.DB) error {
|
|
return duplicateTx.Create(&PrefillGroup{
|
|
Name: original.Name,
|
|
Type: "model",
|
|
Items: JSONValue(`[]`),
|
|
}).Error
|
|
})
|
|
require.Error(t, duplicateError)
|
|
|
|
require.NoError(t, tx.Delete(&original).Error)
|
|
require.NoError(t, tx.Create(&PrefillGroup{
|
|
Name: original.Name,
|
|
Type: "model",
|
|
Items: JSONValue(`[]`),
|
|
}).Error)
|
|
|
|
var totalRows int64
|
|
require.NoError(t, tx.Unscoped().Model(&PrefillGroup{}).Count(&totalRows).Error)
|
|
assert.EqualValues(t, 2, totalRows)
|
|
})
|
|
}
|
|
}
|