Skip to content

Commit

Permalink
feat(schema): schema diffing (#21374)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronc authored Aug 23, 2024
1 parent 8ddea56 commit 51b63f7
Show file tree
Hide file tree
Showing 9 changed files with 1,137 additions and 0 deletions.
126 changes: 126 additions & 0 deletions schema/diff/diff.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 51b63f7

Please sign in to comment.