mirror of https://github.com/go-gorm/gorm.git
fix: deterministic index ordering when migrating (#7208)
Issue: We observed that, when creating a database based on the same gORM schema multiple times, indexes could appear in different orders, hurting determinism for use-cases like schema comparison. In order to fix this, it's simple to switch the ParseIndexes function to return a list of indices rather than a map, so the callers will iterate in deterministic order.
This commit is contained in:
parent
6bfccf8afa
commit
f482f25c71
|
@ -27,8 +27,9 @@ type IndexOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseIndexes parse schema indexes
|
// ParseIndexes parse schema indexes
|
||||||
func (schema *Schema) ParseIndexes() map[string]Index {
|
func (schema *Schema) ParseIndexes() []*Index {
|
||||||
indexes := map[string]Index{}
|
indexesByName := map[string]*Index{}
|
||||||
|
indexes := []*Index{}
|
||||||
|
|
||||||
for _, field := range schema.Fields {
|
for _, field := range schema.Fields {
|
||||||
if field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUEINDEX"] != "" {
|
if field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUEINDEX"] != "" {
|
||||||
|
@ -38,7 +39,12 @@ func (schema *Schema) ParseIndexes() map[string]Index {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
for _, index := range fieldIndexes {
|
for _, index := range fieldIndexes {
|
||||||
idx := indexes[index.Name]
|
idx := indexesByName[index.Name]
|
||||||
|
if idx == nil {
|
||||||
|
idx = &Index{Name: index.Name}
|
||||||
|
indexesByName[index.Name] = idx
|
||||||
|
indexes = append(indexes, idx)
|
||||||
|
}
|
||||||
idx.Name = index.Name
|
idx.Name = index.Name
|
||||||
if idx.Class == "" {
|
if idx.Class == "" {
|
||||||
idx.Class = index.Class
|
idx.Class = index.Class
|
||||||
|
@ -60,8 +66,6 @@ func (schema *Schema) ParseIndexes() map[string]Index {
|
||||||
sort.Slice(idx.Fields, func(i, j int) bool {
|
sort.Slice(idx.Fields, func(i, j int) bool {
|
||||||
return idx.Fields[i].priority < idx.Fields[j].priority
|
return idx.Fields[i].priority < idx.Fields[j].priority
|
||||||
})
|
})
|
||||||
|
|
||||||
indexes[index.Name] = idx
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,15 +80,14 @@ func (schema *Schema) ParseIndexes() map[string]Index {
|
||||||
func (schema *Schema) LookIndex(name string) *Index {
|
func (schema *Schema) LookIndex(name string) *Index {
|
||||||
if schema != nil {
|
if schema != nil {
|
||||||
indexes := schema.ParseIndexes()
|
indexes := schema.ParseIndexes()
|
||||||
|
|
||||||
if index, found := indexes[name]; found {
|
|
||||||
return &index
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, index := range indexes {
|
for _, index := range indexes {
|
||||||
|
if index.Name == name {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
for _, field := range index.Fields {
|
for _, field := range index.Fields {
|
||||||
if field.Name == name {
|
if field.Name == name {
|
||||||
return &index
|
return index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,17 +61,17 @@ func TestParseIndex(t *testing.T) {
|
||||||
t.Fatalf("failed to parse user index, got error %v", err)
|
t.Fatalf("failed to parse user index, got error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
results := map[string]schema.Index{
|
results := []*schema.Index{
|
||||||
"idx_user_indices_name": {
|
{
|
||||||
Name: "idx_user_indices_name",
|
Name: "idx_user_indices_name",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name"}}},
|
||||||
},
|
},
|
||||||
"idx_name": {
|
{
|
||||||
Name: "idx_name",
|
Name: "idx_name",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name2", UniqueIndex: "idx_name"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name2", UniqueIndex: "idx_name"}}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_name3": {
|
{
|
||||||
Name: "idx_user_indices_name3",
|
Name: "idx_user_indices_name3",
|
||||||
Type: "btree",
|
Type: "btree",
|
||||||
Where: "name3 != 'jinzhu'",
|
Where: "name3 != 'jinzhu'",
|
||||||
|
@ -82,19 +82,19 @@ func TestParseIndex(t *testing.T) {
|
||||||
Length: 10,
|
Length: 10,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_name4": {
|
{
|
||||||
Name: "idx_user_indices_name4",
|
Name: "idx_user_indices_name4",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name4", UniqueIndex: "idx_user_indices_name4"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name4", UniqueIndex: "idx_user_indices_name4"}}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_name5": {
|
{
|
||||||
Name: "idx_user_indices_name5",
|
Name: "idx_user_indices_name5",
|
||||||
Class: "FULLTEXT",
|
Class: "FULLTEXT",
|
||||||
Comment: "hello , world",
|
Comment: "hello , world",
|
||||||
Where: "age > 10",
|
Where: "age > 10",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name5"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name5"}}},
|
||||||
},
|
},
|
||||||
"profile": {
|
{
|
||||||
Name: "profile",
|
Name: "profile",
|
||||||
Comment: "hello , world",
|
Comment: "hello , world",
|
||||||
Where: "age > 10",
|
Where: "age > 10",
|
||||||
|
@ -104,21 +104,21 @@ func TestParseIndex(t *testing.T) {
|
||||||
Expression: "ABS(age)",
|
Expression: "ABS(age)",
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
"idx_id": {
|
{
|
||||||
Name: "idx_id",
|
Name: "idx_id",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "MemberNumber"}}, {Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "MemberNumber"}}, {Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
|
||||||
},
|
},
|
||||||
"idx_oid": {
|
{
|
||||||
Name: "idx_oid",
|
Name: "idx_oid",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
|
||||||
},
|
},
|
||||||
"type": {
|
{
|
||||||
Name: "type",
|
Name: "type",
|
||||||
Type: "",
|
Type: "",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name7"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name7"}}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_name8": {
|
{
|
||||||
Name: "idx_user_indices_name8",
|
Name: "idx_user_indices_name8",
|
||||||
Type: "",
|
Type: "",
|
||||||
Fields: []schema.IndexOption{
|
Fields: []schema.IndexOption{
|
||||||
|
@ -127,7 +127,16 @@ func TestParseIndex(t *testing.T) {
|
||||||
{Field: &schema.Field{Name: "Name8"}, Collate: "utf8"},
|
{Field: &schema.Field{Name: "Name8"}, Collate: "utf8"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"idx_user_indices_comp_id0": {
|
{
|
||||||
|
Class: "UNIQUE",
|
||||||
|
Name: "idx_user_indices_idx_compname_1",
|
||||||
|
Option: "NULLS NOT DISTINCT",
|
||||||
|
Fields: []schema.IndexOption{
|
||||||
|
{Field: &schema.Field{Name: "CompName1", NotNull: true}},
|
||||||
|
{Field: &schema.Field{Name: "CompName2"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
Name: "idx_user_indices_comp_id0",
|
Name: "idx_user_indices_comp_id0",
|
||||||
Type: "",
|
Type: "",
|
||||||
Fields: []schema.IndexOption{{
|
Fields: []schema.IndexOption{{
|
||||||
|
@ -136,7 +145,7 @@ func TestParseIndex(t *testing.T) {
|
||||||
Field: &schema.Field{Name: "Data0B"},
|
Field: &schema.Field{Name: "Data0B"},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_comp_id1": {
|
{
|
||||||
Name: "idx_user_indices_comp_id1",
|
Name: "idx_user_indices_comp_id1",
|
||||||
Fields: []schema.IndexOption{{
|
Fields: []schema.IndexOption{{
|
||||||
Field: &schema.Field{Name: "Data1A"},
|
Field: &schema.Field{Name: "Data1A"},
|
||||||
|
@ -146,7 +155,7 @@ func TestParseIndex(t *testing.T) {
|
||||||
Field: &schema.Field{Name: "Data1C"},
|
Field: &schema.Field{Name: "Data1C"},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_comp_id2": {
|
{
|
||||||
Name: "idx_user_indices_comp_id2",
|
Name: "idx_user_indices_comp_id2",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{
|
Fields: []schema.IndexOption{{
|
||||||
|
@ -157,15 +166,6 @@ func TestParseIndex(t *testing.T) {
|
||||||
Field: &schema.Field{Name: "Data2B"},
|
Field: &schema.Field{Name: "Data2B"},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
"idx_user_indices_idx_compname_1": {
|
|
||||||
Class: "UNIQUE",
|
|
||||||
Name: "idx_user_indices_idx_compname_1",
|
|
||||||
Option: "NULLS NOT DISTINCT",
|
|
||||||
Fields: []schema.IndexOption{
|
|
||||||
{Field: &schema.Field{Name: "CompName1", NotNull: true}},
|
|
||||||
{Field: &schema.Field{Name: "CompName2"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckIndices(t, results, user.ParseIndexes())
|
CheckIndices(t, results, user.ParseIndexes())
|
||||||
|
@ -195,17 +195,17 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
|
||||||
t.Fatalf("failed to parse user index, got error %v", err)
|
t.Fatalf("failed to parse user index, got error %v", err)
|
||||||
}
|
}
|
||||||
indices := indexSchema.ParseIndexes()
|
indices := indexSchema.ParseIndexes()
|
||||||
CheckIndices(t, map[string]schema.Index{
|
expectedIndices := []*schema.Index{
|
||||||
"idx_index_tests_field_a": {
|
{
|
||||||
Name: "idx_index_tests_field_a",
|
Name: "idx_index_tests_field_a",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldA", Unique: true}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldA", Unique: true}}},
|
||||||
},
|
},
|
||||||
"idx_index_tests_field_c": {
|
{
|
||||||
Name: "idx_index_tests_field_c",
|
Name: "idx_index_tests_field_c",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldC", UniqueIndex: "idx_index_tests_field_c"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldC", UniqueIndex: "idx_index_tests_field_c"}}},
|
||||||
},
|
},
|
||||||
"idx_index_tests_field_d": {
|
{
|
||||||
Name: "idx_index_tests_field_d",
|
Name: "idx_index_tests_field_d",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{
|
Fields: []schema.IndexOption{
|
||||||
|
@ -214,7 +214,7 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
|
||||||
{Field: &schema.Field{Name: "FieldD"}},
|
{Field: &schema.Field{Name: "FieldD"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"uniq_field_e1_e2": {
|
{
|
||||||
Name: "uniq_field_e1_e2",
|
Name: "uniq_field_e1_e2",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{
|
Fields: []schema.IndexOption{
|
||||||
|
@ -222,11 +222,7 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
|
||||||
{Field: &schema.Field{Name: "FieldE2"}},
|
{Field: &schema.Field{Name: "FieldE2"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"idx_index_tests_field_f1": {
|
{
|
||||||
Name: "idx_index_tests_field_f1",
|
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldF1"}}},
|
|
||||||
},
|
|
||||||
"uniq_field_f1_f2": {
|
|
||||||
Name: "uniq_field_f1_f2",
|
Name: "uniq_field_f1_f2",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{
|
Fields: []schema.IndexOption{
|
||||||
|
@ -234,12 +230,16 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
|
||||||
{Field: &schema.Field{Name: "FieldF2"}},
|
{Field: &schema.Field{Name: "FieldF2"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"idx_index_tests_field_g": {
|
{
|
||||||
|
Name: "idx_index_tests_field_f1",
|
||||||
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldF1"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
Name: "idx_index_tests_field_g",
|
Name: "idx_index_tests_field_g",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldG", Unique: true, UniqueIndex: "idx_index_tests_field_g"}}},
|
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldG", Unique: true, UniqueIndex: "idx_index_tests_field_g"}}},
|
||||||
},
|
},
|
||||||
"uniq_field_h1_h2": {
|
{
|
||||||
Name: "uniq_field_h1_h2",
|
Name: "uniq_field_h1_h2",
|
||||||
Class: "UNIQUE",
|
Class: "UNIQUE",
|
||||||
Fields: []schema.IndexOption{
|
Fields: []schema.IndexOption{
|
||||||
|
@ -247,20 +247,23 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
|
||||||
{Field: &schema.Field{Name: "FieldH2"}},
|
{Field: &schema.Field{Name: "FieldH2"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, indices)
|
}
|
||||||
|
CheckIndices(t, expectedIndices, indices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckIndices(t *testing.T, expected, actual map[string]schema.Index) {
|
func CheckIndices(t *testing.T, expected, actual []*schema.Index) {
|
||||||
for k, ei := range expected {
|
if len(expected) != len(actual) {
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Errorf("expected %d indices, but got %d", len(expected), len(actual))
|
||||||
ai, ok := actual[k]
|
return
|
||||||
if !ok {
|
}
|
||||||
t.Errorf("expected index %q but actual missing", k)
|
|
||||||
return
|
for i, ei := range expected {
|
||||||
}
|
t.Run(ei.Name, func(t *testing.T) {
|
||||||
|
ai := actual[i]
|
||||||
tests.AssertObjEqual(t, ai, ei, "Name", "Class", "Type", "Where", "Comment", "Option")
|
tests.AssertObjEqual(t, ai, ei, "Name", "Class", "Type", "Where", "Comment", "Option")
|
||||||
|
|
||||||
if len(ei.Fields) != len(ai.Fields) {
|
if len(ei.Fields) != len(ai.Fields) {
|
||||||
t.Errorf("expected index %q field length is %d but actual %d", k, len(ei.Fields), len(ai.Fields))
|
t.Errorf("expected index %q field length is %d but actual %d", ei.Name, len(ei.Fields), len(ai.Fields))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, ef := range ei.Fields {
|
for i, ef := range ei.Fields {
|
||||||
|
@ -268,9 +271,5 @@ func CheckIndices(t *testing.T, expected, actual map[string]schema.Index) {
|
||||||
tests.AssertObjEqual(t, af, ef, "Name", "Unique", "UniqueIndex", "Expression", "Sort", "Collate", "Length", "NotNull")
|
tests.AssertObjEqual(t, af, ef, "Name", "Unique", "UniqueIndex", "Expression", "Sort", "Collate", "Length", "NotNull")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
delete(actual, k)
|
|
||||||
}
|
|
||||||
for k := range actual {
|
|
||||||
t.Errorf("unexpected index %q", k)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue