Skip to content

Commit b5b244b

Browse files
committed
fix: deterministic index ordering when migrating
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.
1 parent 6bfccf8 commit b5b244b

File tree

2 files changed

+62
-60
lines changed

2 files changed

+62
-60
lines changed

schema/index.go

+14-11
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ type IndexOption struct {
2727
}
2828

2929
// ParseIndexes parse schema indexes
30-
func (schema *Schema) ParseIndexes() map[string]Index {
31-
indexes := map[string]Index{}
30+
func (schema *Schema) ParseIndexes() []*Index {
31+
indexesByName := map[string]*Index{}
32+
indexes := []*Index{}
3233

3334
for _, field := range schema.Fields {
3435
if field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUEINDEX"] != "" {
@@ -38,7 +39,12 @@ func (schema *Schema) ParseIndexes() map[string]Index {
3839
break
3940
}
4041
for _, index := range fieldIndexes {
41-
idx := indexes[index.Name]
42+
idx := indexesByName[index.Name]
43+
if idx == nil {
44+
idx = &Index{Name: index.Name}
45+
indexesByName[index.Name] = idx
46+
indexes = append(indexes, idx)
47+
}
4248
idx.Name = index.Name
4349
if idx.Class == "" {
4450
idx.Class = index.Class
@@ -60,8 +66,6 @@ func (schema *Schema) ParseIndexes() map[string]Index {
6066
sort.Slice(idx.Fields, func(i, j int) bool {
6167
return idx.Fields[i].priority < idx.Fields[j].priority
6268
})
63-
64-
indexes[index.Name] = idx
6569
}
6670
}
6771
}
@@ -76,15 +80,14 @@ func (schema *Schema) ParseIndexes() map[string]Index {
7680
func (schema *Schema) LookIndex(name string) *Index {
7781
if schema != nil {
7882
indexes := schema.ParseIndexes()
79-
80-
if index, found := indexes[name]; found {
81-
return &index
82-
}
83-
8483
for _, index := range indexes {
84+
if index.Name == name {
85+
return index
86+
}
87+
8588
for _, field := range index.Fields {
8689
if field.Name == name {
87-
return &index
90+
return index
8891
}
8992
}
9093
}

schema/index_test.go

+48-49
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,17 @@ func TestParseIndex(t *testing.T) {
6161
t.Fatalf("failed to parse user index, got error %v", err)
6262
}
6363

64-
results := map[string]schema.Index{
65-
"idx_user_indices_name": {
64+
results := []*schema.Index{
65+
{
6666
Name: "idx_user_indices_name",
6767
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name"}}},
6868
},
69-
"idx_name": {
69+
{
7070
Name: "idx_name",
7171
Class: "UNIQUE",
7272
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name2", UniqueIndex: "idx_name"}}},
7373
},
74-
"idx_user_indices_name3": {
74+
{
7575
Name: "idx_user_indices_name3",
7676
Type: "btree",
7777
Where: "name3 != 'jinzhu'",
@@ -82,19 +82,19 @@ func TestParseIndex(t *testing.T) {
8282
Length: 10,
8383
}},
8484
},
85-
"idx_user_indices_name4": {
85+
{
8686
Name: "idx_user_indices_name4",
8787
Class: "UNIQUE",
8888
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name4", UniqueIndex: "idx_user_indices_name4"}}},
8989
},
90-
"idx_user_indices_name5": {
90+
{
9191
Name: "idx_user_indices_name5",
9292
Class: "FULLTEXT",
9393
Comment: "hello , world",
9494
Where: "age > 10",
9595
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name5"}}},
9696
},
97-
"profile": {
97+
{
9898
Name: "profile",
9999
Comment: "hello , world",
100100
Where: "age > 10",
@@ -104,21 +104,21 @@ func TestParseIndex(t *testing.T) {
104104
Expression: "ABS(age)",
105105
}},
106106
},
107-
"idx_id": {
107+
{
108108
Name: "idx_id",
109109
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "MemberNumber"}}, {Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
110110
},
111-
"idx_oid": {
111+
{
112112
Name: "idx_oid",
113113
Class: "UNIQUE",
114114
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "OID", UniqueIndex: "idx_oid"}}},
115115
},
116-
"type": {
116+
{
117117
Name: "type",
118118
Type: "",
119119
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "Name7"}}},
120120
},
121-
"idx_user_indices_name8": {
121+
{
122122
Name: "idx_user_indices_name8",
123123
Type: "",
124124
Fields: []schema.IndexOption{
@@ -127,7 +127,16 @@ func TestParseIndex(t *testing.T) {
127127
{Field: &schema.Field{Name: "Name8"}, Collate: "utf8"},
128128
},
129129
},
130-
"idx_user_indices_comp_id0": {
130+
{
131+
Class: "UNIQUE",
132+
Name: "idx_user_indices_idx_compname_1",
133+
Option: "NULLS NOT DISTINCT",
134+
Fields: []schema.IndexOption{
135+
{Field: &schema.Field{Name: "CompName1", NotNull: true}},
136+
{Field: &schema.Field{Name: "CompName2"}},
137+
},
138+
},
139+
{
131140
Name: "idx_user_indices_comp_id0",
132141
Type: "",
133142
Fields: []schema.IndexOption{{
@@ -136,7 +145,7 @@ func TestParseIndex(t *testing.T) {
136145
Field: &schema.Field{Name: "Data0B"},
137146
}},
138147
},
139-
"idx_user_indices_comp_id1": {
148+
{
140149
Name: "idx_user_indices_comp_id1",
141150
Fields: []schema.IndexOption{{
142151
Field: &schema.Field{Name: "Data1A"},
@@ -146,7 +155,7 @@ func TestParseIndex(t *testing.T) {
146155
Field: &schema.Field{Name: "Data1C"},
147156
}},
148157
},
149-
"idx_user_indices_comp_id2": {
158+
{
150159
Name: "idx_user_indices_comp_id2",
151160
Class: "UNIQUE",
152161
Fields: []schema.IndexOption{{
@@ -157,15 +166,6 @@ func TestParseIndex(t *testing.T) {
157166
Field: &schema.Field{Name: "Data2B"},
158167
}},
159168
},
160-
"idx_user_indices_idx_compname_1": {
161-
Class: "UNIQUE",
162-
Name: "idx_user_indices_idx_compname_1",
163-
Option: "NULLS NOT DISTINCT",
164-
Fields: []schema.IndexOption{
165-
{Field: &schema.Field{Name: "CompName1", NotNull: true}},
166-
{Field: &schema.Field{Name: "CompName2"}},
167-
},
168-
},
169169
}
170170

171171
CheckIndices(t, results, user.ParseIndexes())
@@ -195,17 +195,17 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
195195
t.Fatalf("failed to parse user index, got error %v", err)
196196
}
197197
indices := indexSchema.ParseIndexes()
198-
CheckIndices(t, map[string]schema.Index{
199-
"idx_index_tests_field_a": {
198+
expectedIndices := []*schema.Index{
199+
{
200200
Name: "idx_index_tests_field_a",
201201
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldA", Unique: true}}},
202202
},
203-
"idx_index_tests_field_c": {
203+
{
204204
Name: "idx_index_tests_field_c",
205205
Class: "UNIQUE",
206206
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldC", UniqueIndex: "idx_index_tests_field_c"}}},
207207
},
208-
"idx_index_tests_field_d": {
208+
{
209209
Name: "idx_index_tests_field_d",
210210
Class: "UNIQUE",
211211
Fields: []schema.IndexOption{
@@ -214,63 +214,62 @@ func TestParseIndexWithUniqueIndexAndUnique(t *testing.T) {
214214
{Field: &schema.Field{Name: "FieldD"}},
215215
},
216216
},
217-
"uniq_field_e1_e2": {
217+
{
218218
Name: "uniq_field_e1_e2",
219219
Class: "UNIQUE",
220220
Fields: []schema.IndexOption{
221221
{Field: &schema.Field{Name: "FieldE1"}},
222222
{Field: &schema.Field{Name: "FieldE2"}},
223223
},
224224
},
225-
"idx_index_tests_field_f1": {
226-
Name: "idx_index_tests_field_f1",
227-
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldF1"}}},
228-
},
229-
"uniq_field_f1_f2": {
225+
{
230226
Name: "uniq_field_f1_f2",
231227
Class: "UNIQUE",
232228
Fields: []schema.IndexOption{
233229
{Field: &schema.Field{Name: "FieldF1"}},
234230
{Field: &schema.Field{Name: "FieldF2"}},
235231
},
236232
},
237-
"idx_index_tests_field_g": {
233+
{
234+
Name: "idx_index_tests_field_f1",
235+
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldF1"}}},
236+
},
237+
{
238238
Name: "idx_index_tests_field_g",
239239
Class: "UNIQUE",
240240
Fields: []schema.IndexOption{{Field: &schema.Field{Name: "FieldG", Unique: true, UniqueIndex: "idx_index_tests_field_g"}}},
241241
},
242-
"uniq_field_h1_h2": {
242+
{
243243
Name: "uniq_field_h1_h2",
244244
Class: "UNIQUE",
245245
Fields: []schema.IndexOption{
246246
{Field: &schema.Field{Name: "FieldH1", Unique: true}},
247247
{Field: &schema.Field{Name: "FieldH2"}},
248248
},
249249
},
250-
}, indices)
250+
}
251+
CheckIndices(t, expectedIndices, indices)
251252
}
252253

253-
func CheckIndices(t *testing.T, expected, actual map[string]schema.Index) {
254-
for k, ei := range expected {
255-
t.Run(k, func(t *testing.T) {
256-
ai, ok := actual[k]
257-
if !ok {
258-
t.Errorf("expected index %q but actual missing", k)
259-
return
260-
}
254+
func CheckIndices(t *testing.T, expected, actual []*schema.Index) {
255+
if len(expected) != len(actual) {
256+
t.Errorf("expected %d indices, but got %d", len(expected), len(actual))
257+
return
258+
}
259+
260+
for i, ei := range expected {
261+
t.Run(ei.Name, func(t *testing.T) {
262+
ai := actual[i]
261263
tests.AssertObjEqual(t, ai, ei, "Name", "Class", "Type", "Where", "Comment", "Option")
264+
262265
if len(ei.Fields) != len(ai.Fields) {
263-
t.Errorf("expected index %q field length is %d but actual %d", k, len(ei.Fields), len(ai.Fields))
266+
t.Errorf("expected index %q field length is %d but actual %d", ei.Name, len(ei.Fields), len(ai.Fields))
264267
return
265268
}
266269
for i, ef := range ei.Fields {
267270
af := ai.Fields[i]
268271
tests.AssertObjEqual(t, af, ef, "Name", "Unique", "UniqueIndex", "Expression", "Sort", "Collate", "Length", "NotNull")
269272
}
270273
})
271-
delete(actual, k)
272-
}
273-
for k := range actual {
274-
t.Errorf("unexpected index %q", k)
275274
}
276275
}

0 commit comments

Comments
 (0)