Match declared and existing indexes one to one during sync

Sync paired each declared index with the first existing index that had an
equal definition and did not mark that index as taken. Two declared indexes
with the same definition could therefore claim the same database index: the
other one was dropped as unmatched and never recreated, or a missing one was
never added.

Claim an existing index with the same name and an equal definition first,
then let the remaining declared indexes fall back to any unclaimed existing
index with an equal definition, so renamed indexes are still kept as before.
Iterate in name order so the result no longer depends on map order.
This commit is contained in:
dualfroz committed 2026-09-29 15:57:45 +02:00
1 parent ebe852f332
commit 0a2e162014
3 files changed
+291 -20

No files matched your search

+49 -20
View File
@@ -6,6 +6,7 @@ package xorm
import (
"fmt"
"sort"
"strings"
"xorm.io/xorm/dialects"
@@ -451,6 +452,53 @@ func schemaTableForSync(table *schemas.Table) *schemas.Table {
return filtered
}
// matchIndexes pairs every expected index with at most one existing index.
// An existing index with the same name and an equal definition is claimed
// first, so indexes sharing a definition cannot claim each other's database
// index. The remaining expected indexes fall back to any unclaimed existing
// index with an equal definition, which keeps renamed indexes in place.
// It returns the names of the claimed existing indexes and the expected
// indexes that have no counterpart and must be created.
func matchIndexes(expected, existing map[string]*schemas.Index) (map[string]bool, map[string]*schemas.Index) {
claimed := make(map[string]bool, len(existing))
missing := make(map[string]*schemas.Index)
var unmatched []string
for _, name := range sortedIndexNames(expected) {
if actual, ok := existing[name]; ok && expected[name].Equal(actual) {
claimed[name] = true
continue
}
unmatched = append(unmatched, name)
}
existingNames := sortedIndexNames(existing)
for _, name := range unmatched {
index := expected[name]
found := false
for _, existingName := range existingNames {
if !claimed[existingName] && index.Equal(existing[existingName]) {
claimed[existingName] = true
found = true
break
}
}
if !found {
missing[name] = index
}
}
return claimed, missing
}
func sortedIndexNames(indexes map[string]*schemas.Index) []string {
names := make([]string, 0, len(indexes))
for name := range indexes {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Sync the new struct changes to database, this method will automatically add
// table, column, index, unique. but will not delete or change anything.
// If you change some field, you should change the database manually.
@@ -595,26 +643,7 @@ func (session *Session) SyncWithOptions(opts SyncOptions, beans ...any) (*SyncRe
}
}
// indices found in orig table
foundIndexNames := make(map[string]bool)
// indices to be added
addedNames := make(map[string]*schemas.Index)
// drop indices that exist in orig and new table schema but are not equal
for name, index := range syncTable.Indexes {
var oriIndex *schemas.Index
for name2, index2 := range oriTable.Indexes {
if index.Equal(index2) {
oriIndex = index2
foundIndexNames[name2] = true
break
}
}
if oriIndex == nil {
addedNames[name] = index
}
}
foundIndexNames, addedNames := matchIndexes(syncTable.Indexes, oriTable.Indexes)
// drop all indices that do not exist in new schema or have changed
for name2, index2 := range oriTable.Indexes {