migration: Add colorization to drift output (#35735)

This commit is contained in:
Eric Fritz 2022-05-19 17:34:32 -05:00 committed by GitHub
parent 196bac71e9
commit c348526e42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 123 additions and 77 deletions

View File

@ -3,9 +3,9 @@ package cliutil
import (
"context"
"github.com/google/go-cmp/cmp"
"github.com/urfave/cli/v2"
descriptions "github.com/sourcegraph/sourcegraph/internal/database/migration/schemas"
"github.com/sourcegraph/sourcegraph/lib/output"
)
@ -38,13 +38,8 @@ func Drift(commandName string, factory RunnerFactory, outFactory func() *output.
return err
}
if diff := cmp.Diff(prepareForSchemaComparison(schema, expected), expected); diff == "" {
out.Write("No drift detected!")
} else {
out.Writef("Database schema drift detected: %s", diff)
}
return nil
descriptions.Canonicalize(schema)
return compareSchemaDescriptions(out, schema, expected)
})
return &cli.Command{

View File

@ -5,8 +5,12 @@ import (
"fmt"
"net/http"
"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/sourcegraph/internal/database/migration/schemas"
descriptions "github.com/sourcegraph/sourcegraph/internal/database/migration/schemas"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"
)
// fetchSchema returns the schema description of the given schema at the given version. If the version
@ -54,95 +58,142 @@ func getSchemaJSONFilename(schemaName, version string) (string, error) {
return "", errors.Newf("unknown schema name %q", schemaName)
}
// prepareForSchemaComparison modifies the given schema description to minimize the _spurious_ diff
// between the actual and the given expected schema description.
//
// This function currently:
// - removes entire objects that are not present in the expected schema to allow additional extensions
// enums, functions, sequences, tables, and views that may be co-located (dev environments for one).
func prepareForSchemaComparison(schemaDescription, expectedSchemaDescription descriptions.SchemaDescription) descriptions.SchemaDescription {
var (
expectedExtensions = map[string]struct{}{}
expectedEnums = map[string]struct{}{}
expectedFunctions = map[string]struct{}{}
expectedSequences = map[string]struct{}{}
expectedTables = map[string]struct{}{}
expectedViews = map[string]struct{}{}
)
var errOutOfSync = errors.Newf("database schema is out of sync")
for _, extension := range expectedSchemaDescription.Extensions {
func compareSchemaDescriptions(out *output.Output, actual, expected schemas.SchemaDescription) (err error) {
missing := func(typeName, name string, value any) {
out.WriteLine(output.Line(output.EmojiFailure, output.StyleBold, fmt.Sprintf("Missing %s %q", typeName, name)))
jsonValue, _ := json.MarshalIndent(value, "", " ")
out.WriteMarkdown(fmt.Sprintf("```json\n%s\n```", string(jsonValue)))
err = errOutOfSync
}
diff := func(typeName, fieldName, name string, a, b any) {
diff := cmp.Diff(a, b)
if diff != "" {
out.WriteLine(output.Line(output.EmojiFailure, output.StyleBold, fmt.Sprintf("Mismatched %s of %s %q", fieldName, typeName, name)))
out.WriteMarkdown(fmt.Sprintf("```diff\n%s```", diff))
err = errOutOfSync
}
}
//
// Compare extensions
actualExtensions := map[string]struct{}{}
for _, extension := range actual.Extensions {
actualExtensions[extension] = struct{}{}
}
expectedExtensions := map[string]struct{}{}
for _, extension := range expected.Extensions {
expectedExtensions[extension] = struct{}{}
}
for _, enum := range expectedSchemaDescription.Enums {
expectedEnums[enum.Name] = struct{}{}
}
for _, function := range expectedSchemaDescription.Functions {
expectedFunctions[function.Name] = struct{}{}
}
for _, sequence := range expectedSchemaDescription.Sequences {
expectedSequences[sequence.Name] = struct{}{}
}
for _, table := range expectedSchemaDescription.Tables {
expectedTables[table.Name] = struct{}{}
}
for _, view := range expectedSchemaDescription.Views {
expectedViews[view.Name] = struct{}{}
}
var (
filteredExtensions = schemaDescription.Extensions[:0]
filteredEnums = schemaDescription.Enums[:0]
filteredFunctions = schemaDescription.Functions[:0]
filteredSequences = schemaDescription.Sequences[:0]
filteredTables = schemaDescription.Tables[:0]
filteredViews = schemaDescription.Views[:0]
)
for _, extension := range schemaDescription.Extensions {
if _, ok := expectedExtensions[extension]; ok {
filteredExtensions = append(filteredExtensions, extension)
for name := range expectedExtensions {
if _, ok := actualExtensions[name]; !ok {
missing("extension", name, nil)
}
}
for _, enum := range schemaDescription.Enums {
if _, ok := expectedEnums[enum.Name]; ok {
filteredEnums = append(filteredEnums, enum)
//
// Compare enums
actualEnums := map[string]schemas.EnumDescription{}
for _, enum := range actual.Enums {
actualEnums[enum.Name] = enum
}
expectedEnums := map[string]schemas.EnumDescription{}
for _, enum := range expected.Enums {
expectedEnums[enum.Name] = enum
}
for name, expectedEnum := range expectedEnums {
if enum, ok := actualEnums[name]; !ok {
missing("enum", name, expectedEnum)
} else {
diff("enum", "labels", name, expectedEnum.Labels, enum.Labels)
}
}
for _, function := range schemaDescription.Functions {
if _, ok := expectedFunctions[function.Name]; ok {
filteredFunctions = append(filteredFunctions, function)
//
// Compare functions
actualFunctions := map[string]schemas.FunctionDescription{}
for _, function := range actual.Functions {
actualFunctions[function.Name] = function
}
expectedFunctions := map[string]schemas.FunctionDescription{}
for _, function := range expected.Functions {
expectedFunctions[function.Name] = function
}
for name, expectedFunction := range expectedFunctions {
if function, ok := actualFunctions[name]; !ok {
missing("function", name, expectedFunction)
} else {
diff("function", "definition", name, expectedFunction.Definition, function.Definition)
}
}
for _, sequence := range schemaDescription.Sequences {
if _, ok := expectedSequences[sequence.Name]; ok {
filteredSequences = append(filteredSequences, sequence)
//
// Compare sequences
actualSequences := map[string]schemas.SequenceDescription{}
for _, sequence := range actual.Sequences {
actualSequences[sequence.Name] = sequence
}
expectedSequences := map[string]schemas.SequenceDescription{}
for _, sequence := range expected.Sequences {
expectedSequences[sequence.Name] = sequence
}
for name, expectedSequence := range expectedSequences {
if sequence, ok := actualSequences[name]; !ok {
missing("sequence", name, expectedSequence)
} else {
diff("sequence", "definition", name, expectedSequence, sequence)
}
}
for _, table := range schemaDescription.Tables {
if _, ok := expectedTables[table.Name]; ok {
filteredTables = append(filteredTables, table)
//
// Compare tables
actualTables := map[string]schemas.TableDescription{}
for _, table := range actual.Tables {
actualTables[table.Name] = table
}
expectedTables := map[string]schemas.TableDescription{}
for _, Table := range expected.Tables {
expectedTables[Table.Name] = Table
}
for name, expectedTable := range expectedTables {
if table, ok := actualTables[name]; !ok {
missing("table", name, expectedTable)
} else {
diff("table", "columns", name, expectedTable.Columns, table.Columns)
diff("table", "columns", name, expectedTable.Constraints, table.Constraints)
diff("table", "columns", name, expectedTable.Indexes, table.Indexes)
diff("table", "columns", name, expectedTable.Triggers, table.Triggers)
}
}
for _, view := range schemaDescription.Views {
if _, ok := expectedViews[view.Name]; ok {
filteredViews = append(filteredViews, view)
//
// Compare views
actualViews := map[string]schemas.ViewDescription{}
for _, view := range actual.Views {
actualViews[view.Name] = view
}
expectedViews := map[string]schemas.ViewDescription{}
for _, view := range expected.Views {
expectedViews[view.Name] = view
}
for name, expectedView := range expectedViews {
if view, ok := actualViews[name]; !ok {
missing("view", name, expectedView)
} else {
diff("view", "definition", name, expectedView.Definition, view.Definition)
}
}
newSchema := descriptions.SchemaDescription{
Extensions: filteredExtensions,
Enums: filteredEnums,
Functions: filteredFunctions,
Sequences: filteredSequences,
Tables: filteredTables,
Views: filteredViews,
if err == nil {
out.Write("No drift detected")
}
descriptions.Canonicalize(newSchema)
return newSchema
return err
}