mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-08-31 02:41:34 +00:00
fix(model): drop leftover prefill_groups unique constraints before AutoMigrate
This commit is contained in:
@@ -81,6 +81,11 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
|
||||
|
||||
**Database compatibility:** All database code MUST work with SQLite, MySQL >= 5.7.8, and PostgreSQL >= 9.6 simultaneously.
|
||||
|
||||
- Any change that can affect database behavior MUST be verified before the work is considered complete. This includes ORM/database-driver dependency changes, connection/DSN/protocol or prepared-statement configuration, models and GORM tags, migrations and `AutoMigrate`, constraints and indexes, `Scanner`/`Valuer`/serializer behavior, raw SQL, transactions, and row locking.
|
||||
- Required database verification MUST exercise real SQLite, MySQL, and PostgreSQL instances. Unit tests, mocks, a successful build, code inspection, or testing only one dialect are not substitutes. Use at least one supported version of each engine; changes that depend on version-specific behavior must also cover the minimum supported version.
|
||||
- Treat GORM core and its database dialect/driver packages as a compatible version set. Any change to one of them requires checking upstream compatibility and running the complete three-database verification matrix; do not upgrade only the core package and infer that existing drivers remain compatible.
|
||||
- Schema or migration changes MUST be tested both on a fresh database and by upgrading a representative database created by the latest released version. Run startup/migration at least twice to prove idempotency, and verify that existing data, indexes, constraints, and uniqueness guarantees are preserved. Cover the separately configured log database when the affected path is shared with or used by it.
|
||||
- Record the exact database versions, commands, and results in the final handoff or pull request. If any required database verification cannot be run, report the blocker explicitly and do not claim the change is database-compatible or complete.
|
||||
- Prefer GORM methods (`Create`, `Find`, `Where`, `Updates`, etc.) over raw SQL.
|
||||
- Let GORM handle primary key generation; do not use `AUTO_INCREMENT` or `SERIAL` directly.
|
||||
- Standard `SELECT ... FOR UPDATE` row locks built with GORM query methods in `model/` MUST use `lockForUpdate(tx)`. Do not use the legacy GORM v1 pattern `tx.Set("gorm:query_option", "FOR UPDATE")`, because GORM v2 silently ignores it and no lock is acquired. Do not duplicate `clause.Locking{Strength: "UPDATE"}` at call sites; the shared helper emits `FOR UPDATE` for MySQL/PostgreSQL and skips it for SQLite, where the syntax is unsupported. Dialect-specific locking with different semantics (for example, a MySQL next-key/gap lock) may use raw SQL only behind explicit database-type branches with valid fallbacks for every supported database.
|
||||
|
||||
+3
-83
@@ -315,6 +315,9 @@ func is64BitIntegerType(dbType common.DatabaseType, dataType string) bool {
|
||||
}
|
||||
|
||||
func migrateDB() error {
|
||||
if err := migratePrefillGroupUniqueness(DB); err != nil {
|
||||
return err
|
||||
}
|
||||
// Migrate price_amount column from float/double to decimal for existing tables
|
||||
migrateSubscriptionPlanPriceAmount()
|
||||
// Migrate model_limits column from varchar to text for existing tables
|
||||
@@ -380,89 +383,6 @@ func migrateDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateDBFast() error {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
migrations := []struct {
|
||||
model interface{}
|
||||
name string
|
||||
}{
|
||||
{&Channel{}, "Channel"},
|
||||
{&Token{}, "Token"},
|
||||
{&User{}, "User"},
|
||||
{&UserSession{}, "UserSession"},
|
||||
{&AuthFlow{}, "AuthFlow"},
|
||||
{&ExternalIdentityClaim{}, "ExternalIdentityClaim"},
|
||||
{&PasskeyCredential{}, "PasskeyCredential"},
|
||||
{&Option{}, "Option"},
|
||||
{&LoginEncryptionKey{}, "LoginEncryptionKey"},
|
||||
{&Redemption{}, "Redemption"},
|
||||
{&Ability{}, "Ability"},
|
||||
{&Log{}, "Log"},
|
||||
{&Midjourney{}, "Midjourney"},
|
||||
{&TopUp{}, "TopUp"},
|
||||
{&QuotaData{}, "QuotaData"},
|
||||
{&Task{}, "Task"},
|
||||
{&Model{}, "Model"},
|
||||
{&Vendor{}, "Vendor"},
|
||||
{&PrefillGroup{}, "PrefillGroup"},
|
||||
{&Setup{}, "Setup"},
|
||||
{&TwoFA{}, "TwoFA"},
|
||||
{&TwoFABackupCode{}, "TwoFABackupCode"},
|
||||
{&Checkin{}, "Checkin"},
|
||||
{&SubscriptionOrder{}, "SubscriptionOrder"},
|
||||
{&UserSubscription{}, "UserSubscription"},
|
||||
{&SubscriptionPreConsumeRecord{}, "SubscriptionPreConsumeRecord"},
|
||||
{&CustomOAuthProvider{}, "CustomOAuthProvider"},
|
||||
{&UserOAuthBinding{}, "UserOAuthBinding"},
|
||||
{&PerfMetric{}, "PerfMetric"},
|
||||
{&SystemInstance{}, "SystemInstance"},
|
||||
{&SystemTask{}, "SystemTask"},
|
||||
{&SystemTaskLock{}, "SystemTaskLock"},
|
||||
}
|
||||
// 动态计算migration数量,确保errChan缓冲区足够大
|
||||
errChan := make(chan error, len(migrations))
|
||||
|
||||
for _, m := range migrations {
|
||||
wg.Add(1)
|
||||
go func(model interface{}, name string) {
|
||||
defer wg.Done()
|
||||
if err := DB.AutoMigrate(model); err != nil {
|
||||
errChan <- fmt.Errorf("failed to migrate %s: %v", name, err)
|
||||
}
|
||||
}(m.model, m.name)
|
||||
}
|
||||
|
||||
// Wait for all migrations to complete
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for any errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := InitializeUserAuthVersions(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := InitializeExternalIdentityClaims(); err != nil {
|
||||
return err
|
||||
}
|
||||
if common.UsingMainDatabase(common.DatabaseTypeSQLite) {
|
||||
if err := ensureSubscriptionPlanTableSQLite(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := DB.AutoMigrate(&SubscriptionPlan{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
common.SysLog("database migrated")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateLOGDB() error {
|
||||
if common.UsingLogDatabase(common.DatabaseTypeClickHouse) {
|
||||
return migrateClickHouseLogDB()
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const prefillGroupNameIndex = "uk_prefill_name"
|
||||
|
||||
type conflictingPrefillGroupUniqueness struct {
|
||||
constraints []string
|
||||
indexes []string
|
||||
}
|
||||
|
||||
type prefillGroupNameIndexState struct {
|
||||
exists bool
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (conflicts conflictingPrefillGroupUniqueness) empty() bool {
|
||||
return len(conflicts.constraints) == 0 && len(conflicts.indexes) == 0
|
||||
}
|
||||
|
||||
func inspectConflictingPrefillGroupUniqueness(db *gorm.DB, tableName string) (conflictingPrefillGroupUniqueness, error) {
|
||||
var conflicts conflictingPrefillGroupUniqueness
|
||||
if err := db.Raw(`
|
||||
SELECT constraint_meta.conname
|
||||
FROM pg_catalog.pg_constraint AS constraint_meta
|
||||
WHERE constraint_meta.conrelid = to_regclass(?)
|
||||
AND constraint_meta.contype = 'u'
|
||||
AND cardinality(constraint_meta.conkey) = 1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_attribute AS attribute_meta
|
||||
WHERE attribute_meta.attrelid = constraint_meta.conrelid
|
||||
AND attribute_meta.attnum = constraint_meta.conkey[1]
|
||||
AND attribute_meta.attname = ?
|
||||
)
|
||||
ORDER BY constraint_meta.conname`, tableName, "name").Scan(&conflicts.constraints).Error; err != nil {
|
||||
return conflicts, fmt.Errorf("inspect conflicting prefill group unique constraints: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Raw(`
|
||||
SELECT index_class.relname
|
||||
FROM pg_catalog.pg_index AS index_meta
|
||||
JOIN pg_catalog.pg_class AS index_class
|
||||
ON index_class.oid = index_meta.indexrelid
|
||||
JOIN pg_catalog.pg_attribute AS attribute_meta
|
||||
ON attribute_meta.attrelid = index_meta.indrelid
|
||||
AND attribute_meta.attnum = index_meta.indkey[0]
|
||||
WHERE index_meta.indrelid = to_regclass(?)
|
||||
AND index_meta.indisunique
|
||||
AND NOT index_meta.indisprimary
|
||||
AND index_meta.indpred IS NULL
|
||||
AND index_meta.indexprs IS NULL
|
||||
AND index_meta.indnatts = 1
|
||||
AND attribute_meta.attname = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_constraint AS constraint_meta
|
||||
WHERE constraint_meta.conindid = index_meta.indexrelid
|
||||
)
|
||||
ORDER BY index_class.relname`, tableName, "name").Scan(&conflicts.indexes).Error; err != nil {
|
||||
return conflicts, fmt.Errorf("inspect conflicting prefill group unique indexes: %w", err)
|
||||
}
|
||||
|
||||
return conflicts, nil
|
||||
}
|
||||
|
||||
func inspectPrefillGroupNameIndex(db *gorm.DB, tableName string) (prefillGroupNameIndexState, error) {
|
||||
var state struct {
|
||||
Exists bool `gorm:"column:index_exists"`
|
||||
Valid bool `gorm:"column:index_valid"`
|
||||
}
|
||||
if err := db.Raw(`
|
||||
SELECT count(*) > 0 AS index_exists,
|
||||
COALESCE(bool_or(
|
||||
index_meta.indisunique
|
||||
AND index_meta.indisvalid
|
||||
AND index_meta.indisready
|
||||
AND NOT index_meta.indisprimary
|
||||
AND index_meta.indexprs IS NULL
|
||||
AND index_meta.indnatts = 1
|
||||
AND attribute_meta.attname = ?
|
||||
AND pg_get_expr(index_meta.indpred, index_meta.indrelid) = '(deleted_at IS NULL)'
|
||||
), false) AS index_valid
|
||||
FROM pg_catalog.pg_index AS index_meta
|
||||
JOIN pg_catalog.pg_class AS index_class
|
||||
ON index_class.oid = index_meta.indexrelid
|
||||
LEFT JOIN pg_catalog.pg_attribute AS attribute_meta
|
||||
ON attribute_meta.attrelid = index_meta.indrelid
|
||||
AND attribute_meta.attnum = index_meta.indkey[0]
|
||||
WHERE index_meta.indrelid = to_regclass(?)
|
||||
AND index_class.relname = ?`, "name", tableName, prefillGroupNameIndex).Scan(&state).Error; err != nil {
|
||||
return prefillGroupNameIndexState{}, fmt.Errorf("inspect prefill group partial unique index: %w", err)
|
||||
}
|
||||
return prefillGroupNameIndexState{exists: state.Exists, valid: state.Valid}, nil
|
||||
}
|
||||
|
||||
// migratePrefillGroupUniqueness removes global PostgreSQL uniqueness left by
|
||||
// older GORM versions before AutoMigrate reconciles the current partial index.
|
||||
func migratePrefillGroupUniqueness(db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("migrate prefill group uniqueness: database is nil")
|
||||
}
|
||||
if db.Dialector.Name() != "postgres" {
|
||||
return nil
|
||||
}
|
||||
|
||||
statement := &gorm.Statement{DB: db}
|
||||
if err := statement.Parse(&PrefillGroup{}); err != nil {
|
||||
return fmt.Errorf("parse prefill group schema: %w", err)
|
||||
}
|
||||
tableName := statement.Schema.Table
|
||||
conflicts, err := inspectConflictingPrefillGroupUniqueness(db, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conflicts.empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
migrator := tx.Migrator()
|
||||
if !migrator.HasTable(&PrefillGroup{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := tx.Exec(
|
||||
"LOCK TABLE ? IN ACCESS EXCLUSIVE MODE",
|
||||
clause.Table{Name: tableName},
|
||||
).Error; err != nil {
|
||||
return fmt.Errorf("lock prefill groups for uniqueness migration: %w", err)
|
||||
}
|
||||
|
||||
conflicts, err := inspectConflictingPrefillGroupUniqueness(tx, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conflicts.empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !migrator.HasColumn(&PrefillGroup{}, "DeletedAt") {
|
||||
if err := migrator.AddColumn(&PrefillGroup{}, "DeletedAt"); err != nil {
|
||||
return fmt.Errorf("add prefill groups deleted_at column: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, constraintName := range conflicts.constraints {
|
||||
if err := migrator.DropConstraint(&PrefillGroup{}, constraintName); err != nil {
|
||||
return fmt.Errorf("drop conflicting prefill group constraint %q: %w", constraintName, err)
|
||||
}
|
||||
}
|
||||
for _, indexName := range conflicts.indexes {
|
||||
if err := migrator.DropIndex(&PrefillGroup{}, indexName); err != nil {
|
||||
return fmt.Errorf("drop conflicting prefill group index %q: %w", indexName, err)
|
||||
}
|
||||
}
|
||||
|
||||
targetIndex, err := inspectPrefillGroupNameIndex(tx, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !targetIndex.exists {
|
||||
if err := migrator.CreateIndex(&PrefillGroup{}, prefillGroupNameIndex); err != nil {
|
||||
return fmt.Errorf("create prefill group partial unique index: %w", err)
|
||||
}
|
||||
targetIndex, err = inspectPrefillGroupNameIndex(tx, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !targetIndex.valid {
|
||||
return fmt.Errorf("prefill group index %q has an unexpected definition", prefillGroupNameIndex)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
const legacyPrefillGroupNameUnique = "idx_prefill_groups_name"
|
||||
|
||||
func testPrefillGroupMigrationNonPostgreSQL(t *testing.T, db *gorm.DB) {
|
||||
t.Helper()
|
||||
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)
|
||||
|
||||
for range 2 {
|
||||
require.NoError(t, migratePrefillGroupUniqueness(db))
|
||||
require.NoError(t, tableDB.AutoMigrate(&PrefillGroup{}))
|
||||
}
|
||||
|
||||
var preserved PrefillGroup
|
||||
require.NoError(t, tableDB.Where("name = ?", "preserved-name").First(&preserved).Error)
|
||||
assert.Equal(t, "preserve me", preserved.Description)
|
||||
assert.True(t, tableDB.Migrator().HasIndex(&PrefillGroup{}, prefillGroupNameIndex))
|
||||
}
|
||||
|
||||
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()) })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prepareOld func(*testing.T, *gorm.DB)
|
||||
preservedIndexes []string
|
||||
}{
|
||||
{name: "fresh"},
|
||||
{
|
||||
name: "legacy_constraint",
|
||||
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
||||
t.Helper()
|
||||
require.NoError(t, tx.Exec(
|
||||
"ALTER TABLE ? ADD CONSTRAINT ? UNIQUE (?)",
|
||||
clause.Table{Name: "prefill_groups"},
|
||||
clause.Column{Name: "idx_prefill_groups_name"},
|
||||
clause.Column{Name: "name"},
|
||||
).Error)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy_standalone_index",
|
||||
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
||||
t.Helper()
|
||||
require.NoError(t, tx.Migrator().DropIndex(&PrefillGroup{}, prefillGroupNameIndex))
|
||||
require.NoError(t, tx.Exec(
|
||||
"CREATE UNIQUE INDEX ? ON ? (?)",
|
||||
clause.Column{Name: "idx_prefill_groups_name"},
|
||||
clause.Table{Name: "prefill_groups"},
|
||||
clause.Column{Name: "name"},
|
||||
).Error)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arbitrary_constraint_name",
|
||||
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
||||
t.Helper()
|
||||
require.NoError(t, tx.Exec(
|
||||
"ALTER TABLE ? ADD CONSTRAINT ? UNIQUE (?)",
|
||||
clause.Table{Name: "prefill_groups"},
|
||||
clause.Column{Name: "prefill_groups_name_key"},
|
||||
clause.Column{Name: "name"},
|
||||
).Error)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arbitrary_index_name",
|
||||
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
||||
t.Helper()
|
||||
require.NoError(t, tx.Exec(
|
||||
"CREATE UNIQUE INDEX ? ON ? (?)",
|
||||
clause.Column{Name: "prefill_groups_name_key"},
|
||||
clause.Table{Name: "prefill_groups"},
|
||||
clause.Column{Name: "name"},
|
||||
).Error)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non_conflicting_indexes_are_preserved",
|
||||
prepareOld: func(t *testing.T, tx *gorm.DB) {
|
||||
t.Helper()
|
||||
require.NoError(t, tx.Exec(
|
||||
"ALTER TABLE ? ADD CONSTRAINT ? UNIQUE (?)",
|
||||
clause.Table{Name: "prefill_groups"},
|
||||
clause.Column{Name: legacyPrefillGroupNameUnique},
|
||||
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)
|
||||
},
|
||||
preservedIndexes: []string{
|
||||
"keep_prefill_name_deleted_at",
|
||||
"keep_prefill_lower_name",
|
||||
"keep_prefill_deleted_name",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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, 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.prepareOld != nil {
|
||||
test.prepareOld(t, tx)
|
||||
}
|
||||
|
||||
for range 2 {
|
||||
require.NoError(t, migratePrefillGroupUniqueness(tx))
|
||||
require.NoError(t, tx.AutoMigrate(&PrefillGroup{}))
|
||||
}
|
||||
for _, indexName := range test.preservedIndexes {
|
||||
assert.True(t, tx.Migrator().HasIndex(&PrefillGroup{}, indexName))
|
||||
}
|
||||
|
||||
var preserved PrefillGroup
|
||||
require.NoError(t, tx.First(&preserved, original.Id).Error)
|
||||
assert.Equal(t, original.Name, preserved.Name)
|
||||
assert.Equal(t, original.Description, preserved.Description)
|
||||
|
||||
var globalConstraintCount int64
|
||||
require.NoError(t, tx.Raw(`
|
||||
SELECT count(*)
|
||||
FROM pg_catalog.pg_constraint AS constraint_meta
|
||||
WHERE constraint_meta.conrelid = to_regclass('prefill_groups')
|
||||
AND constraint_meta.contype = 'u'
|
||||
AND cardinality(constraint_meta.conkey) = 1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_attribute AS attribute_meta
|
||||
WHERE attribute_meta.attrelid = constraint_meta.conrelid
|
||||
AND attribute_meta.attnum = constraint_meta.conkey[1]
|
||||
AND attribute_meta.attname = 'name'
|
||||
)`).Scan(&globalConstraintCount).Error)
|
||||
assert.Zero(t, globalConstraintCount)
|
||||
|
||||
var globalIndexCount int64
|
||||
require.NoError(t, tx.Raw(`
|
||||
SELECT count(*)
|
||||
FROM pg_catalog.pg_index AS index_meta
|
||||
JOIN pg_catalog.pg_attribute AS attribute_meta
|
||||
ON attribute_meta.attrelid = index_meta.indrelid
|
||||
AND attribute_meta.attnum = index_meta.indkey[0]
|
||||
WHERE index_meta.indrelid = to_regclass('prefill_groups')
|
||||
AND index_meta.indisunique
|
||||
AND NOT index_meta.indisprimary
|
||||
AND index_meta.indpred IS NULL
|
||||
AND index_meta.indexprs IS NULL
|
||||
AND index_meta.indnatts = 1
|
||||
AND attribute_meta.attname = 'name'`).Scan(&globalIndexCount).Error)
|
||||
assert.Zero(t, globalIndexCount)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user