feat(schema): schema diffing (#21374)

This commit is contained in:
Aaron Craelius 2024-08-23 11:02:54 -04:00 committed by GitHub
parent 8ddea56bb2
commit 51b63f7ffb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1137 additions and 0 deletions

126
schema/diff/diff.go Normal file
View File

@ -0,0 +1,126 @@
package diff
import "cosmossdk.io/schema"
// ModuleSchemaDiff represents the difference between two module schemas.
type ModuleSchemaDiff struct {
// AddedObjectTypes is a list of object types that were added.
AddedObjectTypes []schema.ObjectType
// ChangedObjectTypes is a list of object types that were changed.
ChangedObjectTypes []ObjectTypeDiff
// RemovedObjectTypes is a list of object types that were removed.
RemovedObjectTypes []schema.ObjectType
// AddedEnumTypes is a list of enum types that were added.
AddedEnumTypes []schema.EnumType
// ChangedEnumTypes is a list of enum types that were changed.
ChangedEnumTypes []EnumTypeDiff
// RemovedEnumTypes is a list of enum types that were removed.
RemovedEnumTypes []schema.EnumType
}
// CompareModuleSchemas compares an old and a new module schemas and returns the difference between them.
// If the schemas are equivalent, the Empty method of the returned ModuleSchemaDiff will return true.
//
// Indexer implementations can use these diffs to perform automatic schema migration.
// The specific supported changes that a specific indexer supports are defined by that indexer implementation.
// However, as a general rule, it is suggested that indexers support the following changes to module schemas:
// - Adding object types
// - Adding enum types
// - Adding nullable value fields to object types
// - Adding enum values to enum types
//
// These changes are officially considered "compatible" changes, and the HasCompatibleChanges method of the returned
// ModuleSchemaDiff will return true if only compatible changes are present.
// Module authors can use the above guidelines as a reference point for what changes are generally
// considered safe to make to a module schema without breaking existing indexers.
func CompareModuleSchemas(oldSchema, newSchema schema.ModuleSchema) ModuleSchemaDiff {
diff := ModuleSchemaDiff{}
oldSchema.ObjectTypes(func(oldObj schema.ObjectType) bool {
newTyp, found := newSchema.LookupType(oldObj.Name)
newObj, typeMatch := newTyp.(schema.ObjectType)
if !found || !typeMatch {
diff.RemovedObjectTypes = append(diff.RemovedObjectTypes, oldObj)
return true
}
objDiff := compareObjectType(oldObj, newObj)
if !objDiff.Empty() {
diff.ChangedObjectTypes = append(diff.ChangedObjectTypes, objDiff)
}
return true
})
newSchema.ObjectTypes(func(newObj schema.ObjectType) bool {
oldTyp, found := oldSchema.LookupType(newObj.TypeName())
_, typeMatch := oldTyp.(schema.ObjectType)
if !found || !typeMatch {
diff.AddedObjectTypes = append(diff.AddedObjectTypes, newObj)
}
return true
})
oldSchema.EnumTypes(func(oldEnum schema.EnumType) bool {
newTyp, found := newSchema.LookupType(oldEnum.Name)
newEnum, typeMatch := newTyp.(schema.EnumType)
if !found || !typeMatch {
diff.RemovedEnumTypes = append(diff.RemovedEnumTypes, oldEnum)
return true
}
enumDiff := compareEnumType(oldEnum, newEnum)
if !enumDiff.Empty() {
diff.ChangedEnumTypes = append(diff.ChangedEnumTypes, enumDiff)
}
return true
})
newSchema.EnumTypes(func(newEnum schema.EnumType) bool {
oldTyp, found := oldSchema.LookupType(newEnum.TypeName())
_, typeMatch := oldTyp.(schema.EnumType)
if !found || !typeMatch {
diff.AddedEnumTypes = append(diff.AddedEnumTypes, newEnum)
}
return true
})
return diff
}
func (m ModuleSchemaDiff) Empty() bool {
return len(m.AddedObjectTypes) == 0 &&
len(m.ChangedObjectTypes) == 0 &&
len(m.RemovedObjectTypes) == 0 &&
len(m.AddedEnumTypes) == 0 &&
len(m.ChangedEnumTypes) == 0 &&
len(m.RemovedEnumTypes) == 0
}
// HasCompatibleChanges returns true if the diff contains only compatible changes.
// Compatible changes are changes that are generally safe to make to a module schema without breaking existing indexers
// and indexers should aim to automatically migrate to such changes.
// See the CompareModuleSchemas function for a list of changes that are considered compatible.
func (m ModuleSchemaDiff) HasCompatibleChanges() bool {
// object and enum types can be added but not removed
// changed object and enum types must have compatible changes
if len(m.RemovedObjectTypes) != 0 || len(m.RemovedEnumTypes) != 0 {
return false
}
for _, objectType := range m.ChangedObjectTypes {
if !objectType.HasCompatibleChanges() {
return false
}
}
for _, enumType := range m.ChangedEnumTypes {
if !enumType.HasCompatibleChanges() {
return false
}
}
return true
}

334
schema/diff/diff_test.go Normal file
View File

@ -0,0 +1,334 @@
package diff
import (
"reflect"
"testing"
"cosmossdk.io/schema"
)
func TestCompareModuleSchemas(t *testing.T) {
tt := []struct {
name string
oldSchema schema.ModuleSchema
newSchema schema.ModuleSchema
diff ModuleSchemaDiff
hasCompatibleChanges bool
empty bool
}{
{
name: "no change",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
diff: ModuleSchemaDiff{},
hasCompatibleChanges: true,
empty: true,
},
{
name: "object type added",
oldSchema: mustModuleSchema(t),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
diff: ModuleSchemaDiff{
AddedObjectTypes: []schema.ObjectType{
{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
},
},
},
hasCompatibleChanges: true,
},
{
name: "object type removed",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t),
diff: ModuleSchemaDiff{
RemovedObjectTypes: []schema.ObjectType{
{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
},
},
},
hasCompatibleChanges: false,
},
{
name: "object type changed, key field added",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}, {Name: "key2", Kind: schema.StringKind}},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
KeyFieldsDiff: FieldsDiff{
Added: []schema.Field{
{Name: "key2", Kind: schema.StringKind},
},
},
},
},
},
hasCompatibleChanges: false,
},
{
name: "object type changed, nullable value field added",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
ValueFields: []schema.Field{{Name: "value1", Kind: schema.StringKind, Nullable: true}},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
ValueFieldsDiff: FieldsDiff{
Added: []schema.Field{{Name: "value1", Kind: schema.StringKind, Nullable: true}},
},
},
},
},
hasCompatibleChanges: true,
},
{
name: "object type changed, non-nullable value field added",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}},
ValueFields: []schema.Field{{Name: "value1", Kind: schema.StringKind}},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
ValueFieldsDiff: FieldsDiff{
Added: []schema.Field{{Name: "value1", Kind: schema.StringKind}},
},
},
},
},
hasCompatibleChanges: false,
},
{
name: "object type changed, fields reordered",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.StringKind}, {Name: "key2", Kind: schema.StringKind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key2", Kind: schema.StringKind}, {Name: "key1", Kind: schema.StringKind}},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
KeyFieldsDiff: FieldsDiff{
OldOrder: []string{"key1", "key2"},
NewOrder: []string{"key2", "key1"},
},
},
},
},
hasCompatibleChanges: false,
},
{
name: "enum type added, nullable value field added",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.Int32Kind}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.Int32Kind}},
ValueFields: []schema.Field{
{
Name: "value1",
Kind: schema.EnumKind,
EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}},
Nullable: true,
},
},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
ValueFieldsDiff: FieldsDiff{
Added: []schema.Field{
{
Name: "value1",
Kind: schema.EnumKind,
EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}},
Nullable: true,
},
},
},
},
},
AddedEnumTypes: []schema.EnumType{
{Name: "enum1", Values: []string{"a", "b"}},
},
},
hasCompatibleChanges: true,
},
{
name: "enum type removed",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.Int32Kind}},
ValueFields: []schema.Field{
{
Name: "value1",
Kind: schema.EnumKind,
EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}},
},
},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.Int32Kind}},
}),
diff: ModuleSchemaDiff{
ChangedObjectTypes: []ObjectTypeDiff{
{
Name: "object1",
ValueFieldsDiff: FieldsDiff{
Removed: []schema.Field{
{
Name: "value1",
Kind: schema.EnumKind,
EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}},
},
},
},
},
},
RemovedEnumTypes: []schema.EnumType{
{Name: "enum1", Values: []string{"a", "b"}},
},
},
hasCompatibleChanges: false,
},
{
name: "enum value added",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "enum1", Values: []string{"a"}}}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}}}},
}),
diff: ModuleSchemaDiff{
ChangedEnumTypes: []EnumTypeDiff{
{
Name: "enum1",
AddedValues: []string{"b"},
},
},
},
hasCompatibleChanges: true,
},
{
name: "enum value removed",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b", "c"}}}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "object1",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "enum1", Values: []string{"a", "b"}}}},
}),
diff: ModuleSchemaDiff{
ChangedEnumTypes: []EnumTypeDiff{
{
Name: "enum1",
RemovedValues: []string{"c"},
},
},
},
hasCompatibleChanges: false,
},
{
name: "object type and enum type name switched",
oldSchema: mustModuleSchema(t, schema.ObjectType{
Name: "foo",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "bar", Values: []string{"a"}}}},
}),
newSchema: mustModuleSchema(t, schema.ObjectType{
Name: "bar",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "foo", Values: []string{"a"}}}},
}),
diff: ModuleSchemaDiff{
RemovedObjectTypes: []schema.ObjectType{
{
Name: "foo",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "bar", Values: []string{"a"}}}},
}},
AddedObjectTypes: []schema.ObjectType{
{
Name: "bar",
KeyFields: []schema.Field{{Name: "key1", Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "foo", Values: []string{"a"}}}},
},
},
RemovedEnumTypes: []schema.EnumType{
{Name: "bar", Values: []string{"a"}},
},
AddedEnumTypes: []schema.EnumType{
{Name: "foo", Values: []string{"a"}},
},
},
hasCompatibleChanges: false,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
got := CompareModuleSchemas(tc.oldSchema, tc.newSchema)
if !reflect.DeepEqual(got, tc.diff) {
t.Errorf("CompareModuleSchemas() = %v, want %v", got, tc.diff)
}
hasCompatibleChanges := got.HasCompatibleChanges()
if hasCompatibleChanges != tc.hasCompatibleChanges {
t.Errorf("HasCompatibleChanges() = %v, want %v", hasCompatibleChanges, tc.hasCompatibleChanges)
}
if tc.empty != got.Empty() {
t.Errorf("Empty() = %v, want %v", got.Empty(), tc.empty)
}
})
}
}
func mustModuleSchema(t *testing.T, objectTypes ...schema.ObjectType) schema.ModuleSchema {
s, err := schema.NewModuleSchema(objectTypes)
if err != nil {
t.Fatal(err)
}
return s
}

4
schema/diff/doc.go Normal file
View File

@ -0,0 +1,4 @@
// Package diff provides the CompareModuleSchemas function which compares module schemas
// and their types and generates a structured diff. This diff can be used to check
// compatibility between different versions of a module schema.
package diff

53
schema/diff/enum_diff.go Normal file
View File

@ -0,0 +1,53 @@
package diff
import "cosmossdk.io/schema"
// EnumTypeDiff represents the difference between two enum types.
type EnumTypeDiff struct {
// Name is the name of the enum type.
Name string
// AddedValues is a list of values that were added.
AddedValues []string
// RemovedValues is a list of values that were removed.
RemovedValues []string
}
func compareEnumType(oldEnum, newEnum schema.EnumType) EnumTypeDiff {
diff := EnumTypeDiff{
Name: oldEnum.TypeName(),
}
newValues := make(map[string]struct{})
for _, v := range newEnum.Values {
newValues[v] = struct{}{}
}
oldValues := make(map[string]struct{})
for _, v := range oldEnum.Values {
oldValues[v] = struct{}{}
if _, ok := newValues[v]; !ok {
diff.RemovedValues = append(diff.RemovedValues, v)
}
}
for _, v := range newEnum.Values {
if _, ok := oldValues[v]; !ok {
diff.AddedValues = append(diff.AddedValues, v)
}
}
return diff
}
// Empty returns true if the enum type diff has no changes.
func (e EnumTypeDiff) Empty() bool {
return len(e.AddedValues) == 0 && len(e.RemovedValues) == 0
}
// HasCompatibleChanges returns true if the diff contains only compatible changes.
// The only supported compatible change is adding values.
func (e EnumTypeDiff) HasCompatibleChanges() bool {
return len(e.RemovedValues) == 0
}

View File

@ -0,0 +1,69 @@
package diff
import (
"reflect"
"testing"
"cosmossdk.io/schema"
)
func Test_compareEnumType(t *testing.T) {
tt := []struct {
name string
oldEnum schema.EnumType
newEnum schema.EnumType
diff EnumTypeDiff
hasCompatibleChanges bool
}{
{
name: "no change",
oldEnum: schema.EnumType{
Values: []string{"a", "b"},
},
newEnum: schema.EnumType{
Values: []string{"a", "b"},
},
diff: EnumTypeDiff{},
hasCompatibleChanges: true,
},
{
name: "value added",
oldEnum: schema.EnumType{
Values: []string{"a"},
},
newEnum: schema.EnumType{
Values: []string{"a", "b"},
},
diff: EnumTypeDiff{
AddedValues: []string{"b"},
},
hasCompatibleChanges: true,
},
{
name: "value removed",
oldEnum: schema.EnumType{
Values: []string{"a", "b"},
},
newEnum: schema.EnumType{
Values: []string{"a"},
},
diff: EnumTypeDiff{
RemovedValues: []string{"b"},
},
hasCompatibleChanges: false,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
got := compareEnumType(tc.oldEnum, tc.newEnum)
if !reflect.DeepEqual(got, tc.diff) {
t.Errorf("compareEnumType() = %v, want %v", got, tc.diff)
}
hasCompatibleChanges := got.HasCompatibleChanges()
if hasCompatibleChanges != tc.hasCompatibleChanges {
t.Errorf("HasCompatibleChanges() = %v, want %v", hasCompatibleChanges, tc.hasCompatibleChanges)
}
})
}
}

73
schema/diff/field_diff.go Normal file
View File

@ -0,0 +1,73 @@
package diff
import "cosmossdk.io/schema"
// FieldDiff represents the difference between two fields.
// The KindChanged, NullableChanged, and EnumTypeChanged methods can be used to determine
// what specific changes were made to the field.
type FieldDiff struct {
// Name is the name of the field.
Name string
// OldKind is the old kind of the field. It will be InvalidKind if there was no change.
OldKind schema.Kind
// NewKind is the new kind of the field. It will be InvalidKind if there was no change.
NewKind schema.Kind
// OldNullable is the old nullable property of the field.
OldNullable bool
// NewNullable is the new nullable property of the field.
NewNullable bool
// OldEnumType is the name of the old enum type of the field.
// It will be empty if the field is not an enum type or if there was no change.
OldEnumType string
// NewEnumType is the name of the new enum type of the field.
// It will be empty if the field is not an enum type or if there was no change.
NewEnumType string
}
func compareField(oldField, newField schema.Field) FieldDiff {
diff := FieldDiff{
Name: oldField.Name,
}
if oldField.Kind != newField.Kind {
diff.OldKind = oldField.Kind
diff.NewKind = newField.Kind
}
diff.OldNullable = oldField.Nullable
diff.NewNullable = newField.Nullable
if oldField.EnumType.Name != newField.EnumType.Name {
diff.OldEnumType = oldField.EnumType.Name
diff.NewEnumType = newField.EnumType.Name
}
return diff
}
// Empty returns true if the field diff has no changes.
func (d FieldDiff) Empty() bool {
return !d.KindChanged() && !d.NullableChanged() && !d.EnumTypeChanged()
}
// KindChanged returns true if the field kind changed.
func (d FieldDiff) KindChanged() bool {
return d.OldKind != d.NewKind
}
// NullableChanged returns true if the field nullable property changed.
func (d FieldDiff) NullableChanged() bool {
return d.OldNullable != d.NewNullable
}
// EnumTypeChanged returns true if the field enum type changed.
// Note that if the enum type name remained the same but the values of
// the enum type changed, that won't be reported here but rather in the
// ModuleSchemaDiff's ChangedEnumTypes field.
func (d FieldDiff) EnumTypeChanged() bool {
return d.OldEnumType != d.NewEnumType
}

View File

@ -0,0 +1,63 @@
package diff
import (
"fmt"
"reflect"
"testing"
"cosmossdk.io/schema"
)
func Test_compareField(t *testing.T) {
tests := []struct {
oldField schema.Field
newField schema.Field
wantDiff FieldDiff
trueF func(FieldDiff) bool
}{
{
oldField: schema.Field{Kind: schema.Int32Kind},
newField: schema.Field{Kind: schema.Int32Kind},
wantDiff: FieldDiff{},
trueF: FieldDiff.Empty,
},
{
oldField: schema.Field{Kind: schema.StringKind},
newField: schema.Field{Kind: schema.Int32Kind},
wantDiff: FieldDiff{
OldKind: schema.StringKind,
NewKind: schema.Int32Kind,
},
trueF: FieldDiff.KindChanged,
},
{
oldField: schema.Field{Kind: schema.StringKind},
newField: schema.Field{Kind: schema.StringKind, Nullable: true},
wantDiff: FieldDiff{
NewNullable: true,
},
trueF: FieldDiff.NullableChanged,
},
{
oldField: schema.Field{Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "old"}},
newField: schema.Field{Kind: schema.EnumKind, EnumType: schema.EnumType{Name: "new"}},
wantDiff: FieldDiff{
OldEnumType: "old",
NewEnumType: "new",
},
trueF: FieldDiff.EnumTypeChanged,
},
}
for i, tt := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
gotDiff := compareField(tt.oldField, tt.newField)
if !reflect.DeepEqual(gotDiff, tt.wantDiff) {
t.Errorf("compareField() = %v, want %v", gotDiff, tt.wantDiff)
}
if tt.trueF != nil && !tt.trueF(gotDiff) {
t.Errorf("trueF() = false, want true")
}
})
}
}

View File

@ -0,0 +1,146 @@
package diff
import "cosmossdk.io/schema"
// ObjectTypeDiff represents the difference between two object types.
// The Empty method of KeyFieldsDiff and ValueFieldsDiff can be used to determine
// if there were any changes to the key fields or value fields.
type ObjectTypeDiff struct {
// Name is the name of the object type.
Name string
// KeyFieldsDiff is the difference between the key fields of the object type.
KeyFieldsDiff FieldsDiff
// ValueFieldsDiff is the difference between the value fields of the object type.
ValueFieldsDiff FieldsDiff
}
// FieldsDiff represents the difference between two lists of fields.
// Fields will be compared based on name first, and then if there is any
// difference in ordering that will be reported in OldOrder and NewOrder.
// If there is any order change, the OrderChanged method will return true.
// If fields were only added or removed but the order otherwise didn't change,
// then the OldOrder and NewOrder will still be empty.
type FieldsDiff struct {
// Added is a list of fields that were added.
Added []schema.Field
// Changed is a list of fields that were changed.
Changed []FieldDiff
// Removed is a list of fields that were removed.
Removed []schema.Field
// OldOrder is the order of fields in the old list. It will be empty if the order has not changed.
OldOrder []string
// NewOrder is the order of fields in the new list. It will be empty if the order has not changed.
NewOrder []string
}
func compareObjectType(oldObj, newObj schema.ObjectType) ObjectTypeDiff {
diff := ObjectTypeDiff{
Name: oldObj.TypeName(),
}
diff.KeyFieldsDiff = compareFields(oldObj.KeyFields, newObj.KeyFields)
diff.ValueFieldsDiff = compareFields(oldObj.ValueFields, newObj.ValueFields)
return diff
}
func compareFields(oldFields, newFields []schema.Field) FieldsDiff {
diff := FieldsDiff{}
newFieldMap := make(map[string]schema.Field)
for _, f := range newFields {
newFieldMap[f.Name] = f
}
oldFieldMap := make(map[string]schema.Field)
for _, oldField := range oldFields {
oldFieldMap[oldField.Name] = oldField
newField, ok := newFieldMap[oldField.Name]
if !ok {
diff.Removed = append(diff.Removed, oldField)
} else {
fieldDiff := compareField(oldField, newField)
if !fieldDiff.Empty() {
diff.Changed = append(diff.Changed, fieldDiff)
}
}
}
for _, newField := range newFields {
if _, ok := oldFieldMap[newField.Name]; !ok {
diff.Added = append(diff.Added, newField)
}
}
oldOrder := make([]string, 0, len(oldFields))
for _, f := range oldFields {
oldOrder = append(oldOrder, f.Name)
}
orderChanged := false
newOrder := make([]string, 0, len(newFields))
for i, f := range newFields {
newOrder = append(newOrder, f.Name)
if i < len(oldOrder) && f.Name != oldOrder[i] {
orderChanged = true
}
}
if orderChanged {
diff.OldOrder = oldOrder
diff.NewOrder = newOrder
}
return diff
}
// Empty returns true if the object type diff has no changes.
func (o ObjectTypeDiff) Empty() bool {
return o.KeyFieldsDiff.Empty() && o.ValueFieldsDiff.Empty()
}
// HasCompatibleChanges returns true if the diff contains only compatible changes.
// The only supported compatible change is adding nullable value fields.
func (o ObjectTypeDiff) HasCompatibleChanges() bool {
if !o.KeyFieldsDiff.Empty() {
return false
}
if len(o.ValueFieldsDiff.Changed) != 0 ||
len(o.ValueFieldsDiff.Removed) != 0 ||
o.ValueFieldsDiff.OrderChanged() {
return false
}
for _, field := range o.ValueFieldsDiff.Added {
if !field.Nullable {
return false
}
}
return true
}
// Empty returns true if the field diff has no changes.
func (d FieldsDiff) Empty() bool {
if len(d.Added) != 0 || len(d.Changed) != 0 || len(d.Removed) != 0 {
return false
}
return !d.OrderChanged()
}
// OrderChanged returns true if the field order changed.
func (d FieldsDiff) OrderChanged() bool {
if len(d.OldOrder) == 0 && len(d.NewOrder) == 0 {
return false
}
return true
}

View File

@ -0,0 +1,269 @@
package diff
import (
"reflect"
"testing"
"cosmossdk.io/schema"
)
func Test_objectTypeDiff(t *testing.T) {
tt := []struct {
name string
oldType schema.ObjectType
newType schema.ObjectType
diff ObjectTypeDiff
trueF func(ObjectTypeDiff) bool
hasCompatibleChanges bool
}{
{
name: "no change",
oldType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
newType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
diff: ObjectTypeDiff{},
trueF: ObjectTypeDiff.Empty,
hasCompatibleChanges: true,
},
{
name: "key fields changed",
oldType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
newType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "id", Kind: schema.StringKind}},
},
diff: ObjectTypeDiff{
KeyFieldsDiff: FieldsDiff{
Changed: []FieldDiff{
{
Name: "id",
OldKind: schema.Int32Kind,
NewKind: schema.StringKind,
},
},
},
},
trueF: func(d ObjectTypeDiff) bool { return !d.KeyFieldsDiff.Empty() },
hasCompatibleChanges: false,
},
{
name: "value fields changed",
oldType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "name", Kind: schema.StringKind}},
},
newType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "name", Kind: schema.Int32Kind}},
},
diff: ObjectTypeDiff{
ValueFieldsDiff: FieldsDiff{
Changed: []FieldDiff{
{
Name: "name",
OldKind: schema.StringKind,
NewKind: schema.Int32Kind,
},
},
},
},
trueF: func(d ObjectTypeDiff) bool { return !d.ValueFieldsDiff.Empty() },
hasCompatibleChanges: false,
},
{
name: "nullable value field added",
oldType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
newType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind, Nullable: true}},
},
diff: ObjectTypeDiff{
ValueFieldsDiff: FieldsDiff{
Added: []schema.Field{{Name: "name", Kind: schema.StringKind, Nullable: true}},
},
},
trueF: func(d ObjectTypeDiff) bool { return !d.ValueFieldsDiff.Empty() },
hasCompatibleChanges: true,
},
{
name: "non-nullable value field added",
oldType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
newType: schema.ObjectType{
ValueFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
},
diff: ObjectTypeDiff{
ValueFieldsDiff: FieldsDiff{
Added: []schema.Field{{Name: "name", Kind: schema.StringKind}},
},
},
trueF: func(d ObjectTypeDiff) bool { return !d.ValueFieldsDiff.Empty() },
hasCompatibleChanges: false,
},
{
name: "fields reordered",
oldType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
ValueFields: []schema.Field{{Name: "x", Kind: schema.Int32Kind}, {Name: "y", Kind: schema.StringKind}},
},
newType: schema.ObjectType{
KeyFields: []schema.Field{{Name: "name", Kind: schema.StringKind}, {Name: "id", Kind: schema.Int32Kind}},
ValueFields: []schema.Field{{Name: "y", Kind: schema.StringKind}, {Name: "x", Kind: schema.Int32Kind}},
},
diff: ObjectTypeDiff{
KeyFieldsDiff: FieldsDiff{
OldOrder: []string{"id", "name"},
NewOrder: []string{"name", "id"},
},
ValueFieldsDiff: FieldsDiff{
OldOrder: []string{"x", "y"},
NewOrder: []string{"y", "x"},
},
},
trueF: func(d ObjectTypeDiff) bool { return !d.KeyFieldsDiff.Empty() && !d.ValueFieldsDiff.Empty() },
hasCompatibleChanges: false,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
got := compareObjectType(tc.oldType, tc.newType)
if !reflect.DeepEqual(got, tc.diff) {
t.Errorf("compareObjectType() = %v, want %v", got, tc.diff)
}
hasCompatibleChanges := got.HasCompatibleChanges()
if hasCompatibleChanges != tc.hasCompatibleChanges {
t.Errorf("HasCompatibleChanges() = %v, want %v", hasCompatibleChanges, tc.hasCompatibleChanges)
}
})
}
}
func Test_fieldsDiff(t *testing.T) {
tt := []struct {
name string
oldFields []schema.Field
newFields []schema.Field
diff FieldsDiff
}{
{
name: "no change",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
},
{
name: "field added",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
diff: FieldsDiff{
Added: []schema.Field{{Name: "name", Kind: schema.StringKind}},
},
},
{
name: "field removed",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
diff: FieldsDiff{
Removed: []schema.Field{{Name: "name", Kind: schema.StringKind}},
},
},
{
name: "field changed",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.Int32Kind}},
diff: FieldsDiff{
Changed: []FieldDiff{
{
Name: "name",
OldKind: schema.StringKind,
NewKind: schema.Int32Kind,
},
},
},
},
{
name: "field order changed",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.StringKind}},
newFields: []schema.Field{{Name: "name", Kind: schema.StringKind}, {Name: "id", Kind: schema.Int32Kind}},
diff: FieldsDiff{
OldOrder: []string{"id", "name"},
NewOrder: []string{"name", "id"},
},
},
{
name: "field order changed with added fields",
oldFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
newFields: []schema.Field{{Name: "name", Kind: schema.StringKind}, {Name: "id", Kind: schema.Int32Kind}},
diff: FieldsDiff{
Added: []schema.Field{{Name: "name", Kind: schema.StringKind}},
OldOrder: []string{"id"},
NewOrder: []string{"name", "id"},
},
},
{
name: "field order changed with removed fields",
oldFields: []schema.Field{{Name: "name", Kind: schema.StringKind}, {Name: "id", Kind: schema.Int32Kind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
diff: FieldsDiff{
Removed: []schema.Field{{Name: "name", Kind: schema.StringKind}},
OldOrder: []string{"name", "id"},
NewOrder: []string{"id"},
},
},
{
name: "field order changed with changed fields",
oldFields: []schema.Field{{Name: "name", Kind: schema.StringKind}, {Name: "id", Kind: schema.Int32Kind}},
newFields: []schema.Field{{Name: "id", Kind: schema.Int32Kind}, {Name: "name", Kind: schema.Int32Kind}},
diff: FieldsDiff{
Changed: []FieldDiff{
{
Name: "name",
OldKind: schema.StringKind,
NewKind: schema.Int32Kind,
},
},
OldOrder: []string{"name", "id"},
NewOrder: []string{"id", "name"},
},
},
{
name: "added, removed, changed and reordered fields",
oldFields: []schema.Field{
{Name: "id", Kind: schema.Int32Kind},
{Name: "name", Kind: schema.StringKind},
{Name: "age", Kind: schema.Int32Kind},
},
newFields: []schema.Field{
{Name: "name", Kind: schema.Int32Kind},
{Name: "age", Kind: schema.Int32Kind},
{Name: "email", Kind: schema.StringKind},
},
diff: FieldsDiff{
Added: []schema.Field{{Name: "email", Kind: schema.StringKind}},
Removed: []schema.Field{{Name: "id", Kind: schema.Int32Kind}},
Changed: []FieldDiff{
{
Name: "name",
OldKind: schema.StringKind,
NewKind: schema.Int32Kind,
},
},
OldOrder: []string{"id", "name", "age"},
NewOrder: []string{"name", "age", "email"},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
got := compareFields(tc.oldFields, tc.newFields)
if !reflect.DeepEqual(got, tc.diff) {
t.Errorf("compareFields() = %v, want %v", got, tc.diff)
}
})
}
}