🐛 fix(db): allow re-adding models/vendors after soft delete via composite unique indexes
Ensure models and vendors can be re-created after soft deletion by switching to composite unique indexes on (name, deleted_at) and cleaning up legacy single-column unique indexes on MySQL.
Why
- MySQL raised 1062 duplicate key errors when re-adding a soft-deleted model/vendor because the legacy unique index enforced uniqueness on the name column alone (uk_model_name / uk_vendor_name), despite soft deletes.
- Users encountered errors such as:
- Error 1062 (23000): Duplicate entry 'deepseek-chat' for key 'models.uk_model_name'
- Error 1062 (23000): Duplicate entry 'DeepSeek' for key 'vendors.uk_vendor_name'
How
- Model indices:
- model/model_meta.go:
- Model.ModelName → gorm: uniqueIndex:uk_model_name,priority:1
- Model.DeletedAt → gorm: index; uniqueIndex:uk_model_name,priority:2
- Vendor indices:
- model/vendor_meta.go:
- Vendor.Name → gorm: uniqueIndex:uk_vendor_name,priority:1
- Vendor.DeletedAt → gorm: index; uniqueIndex:uk_vendor_name,priority:2
- Migration (automatic, idempotent):
- model/main.go (migrateDB, migrateDBFast):
- On MySQL, drop legacy single-column unique indexes if present:
- ALTER TABLE models DROP INDEX uk_model_name;
- ALTER TABLE vendors DROP INDEX uk_vendor_name;
- Then run AutoMigrate to create composite unique indexes.
- Missing-index errors are ignored to keep the migration safe to run multiple times.
Result
- Users can delete and re-add the same model/vendor name without manual SQL.
- Migration runs automatically at startup; no user action required.
- PostgreSQL and SQLite remain unaffected.
Files changed
- model/model_meta.go
- model/vendor_meta.go
- model/main.go (migrateDB, migrateDBFast)
Testing
- Create model "deepseek-chat" → delete (soft) → re-create → succeeds.
- Create vendor "DeepSeek" → delete (soft) → re-create → succeeds.
Backward compatibility
- Data remains intact; only index definitions are updated.
- Behavior is unchanged except for fixing the uniqueness constraint with soft deletes.
This commit is contained in:
@@ -235,6 +235,12 @@ func InitLogDB() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrateDB() error {
|
func migrateDB() error {
|
||||||
|
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
|
||||||
|
if common.UsingMySQL {
|
||||||
|
// 旧索引可能不存在,忽略删除错误即可
|
||||||
|
_ = DB.Exec("ALTER TABLE models DROP INDEX uk_model_name;").Error
|
||||||
|
_ = DB.Exec("ALTER TABLE vendors DROP INDEX uk_vendor_name;").Error
|
||||||
|
}
|
||||||
if !common.UsingPostgreSQL {
|
if !common.UsingPostgreSQL {
|
||||||
return migrateDBFast()
|
return migrateDBFast()
|
||||||
}
|
}
|
||||||
@@ -264,6 +270,12 @@ func migrateDB() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrateDBFast() error {
|
func migrateDBFast() error {
|
||||||
|
// 修复旧版本留下的唯一索引,允许软删除后重新插入同名记录
|
||||||
|
if common.UsingMySQL {
|
||||||
|
_ = DB.Exec("ALTER TABLE models DROP INDEX uk_model_name;").Error
|
||||||
|
_ = DB.Exec("ALTER TABLE vendors DROP INDEX uk_vendor_name;").Error
|
||||||
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
migrations := []struct {
|
migrations := []struct {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type BoundChannel struct {
|
|||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,where:deleted_at IS NULL"`
|
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"`
|
||||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||||
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
|
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
|
||||||
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
||||||
@@ -44,7 +44,7 @@ type Model struct {
|
|||||||
Status int `json:"status" gorm:"default:1"`
|
Status int `json:"status" gorm:"default:1"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_model_name,priority:2"`
|
||||||
|
|
||||||
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
||||||
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import (
|
|||||||
|
|
||||||
type Vendor struct {
|
type Vendor struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,where:deleted_at IS NULL"`
|
Name string `json:"name" gorm:"size:128;not null;uniqueIndex:uk_vendor_name,priority:1"`
|
||||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
Description string `json:"description,omitempty" gorm:"type:text"`
|
||||||
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
|
Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"`
|
||||||
Status int `json:"status" gorm:"default:1"`
|
Status int `json:"status" gorm:"default:1"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index;uniqueIndex:uk_vendor_name,priority:2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert 创建新的供应商记录
|
// Insert 创建新的供应商记录
|
||||||
|
|||||||
Reference in New Issue
Block a user