Files
relspecgo/pkg/diff/diff.go
T

712 lines
20 KiB
Go

package diff
import (
"fmt"
"reflect"
"sort"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
// sortedKeys returns a map's keys sorted alphabetically, so callers get a
// deterministic iteration order instead of Go's randomized map order.
func sortedKeys[T any](m map[string]T) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// CompareDatabases compares two database models and returns the differences
func CompareDatabases(source, target *models.Database) *DiffResult {
result := &DiffResult{
Source: source.Name,
Target: target.Name,
Schemas: compareSchemas(source.Schemas, target.Schemas),
}
return result
}
func compareSchemas(source, target []*models.Schema) *SchemaDiff {
diff := &SchemaDiff{
Missing: make([]*models.Schema, 0),
Extra: make([]*models.Schema, 0),
Modified: make([]*SchemaChange, 0),
}
sourceMap := make(map[string]*models.Schema)
targetMap := make(map[string]*models.Schema)
for _, s := range source {
sourceMap[s.SQLName()] = s
}
for _, t := range target {
targetMap[t.SQLName()] = t
}
// Find missing and modified schemas
for _, name := range sortedKeys(sourceMap) {
srcSchema := sourceMap[name]
if tgtSchema, exists := targetMap[name]; !exists {
diff.Missing = append(diff.Missing, srcSchema)
} else {
if change := compareSchemaDetails(srcSchema, tgtSchema); change != nil {
diff.Modified = append(diff.Modified, change)
}
}
}
// Find extra schemas
for _, name := range sortedKeys(targetMap) {
tgtSchema := targetMap[name]
if _, exists := sourceMap[name]; !exists {
diff.Extra = append(diff.Extra, tgtSchema)
}
}
return diff
}
func compareSchemaDetails(source, target *models.Schema) *SchemaChange {
change := &SchemaChange{
Name: source.Name,
}
hasChanges := false
// Compare tables
tableDiff := compareTables(source.Tables, target.Tables)
if !isEmpty(tableDiff) {
change.Tables = tableDiff
hasChanges = true
}
// Compare views
viewDiff := compareViews(source.Views, target.Views)
if !isEmpty(viewDiff) {
change.Views = viewDiff
hasChanges = true
}
// Compare sequences
sequenceDiff := compareSequences(source.Sequences, target.Sequences)
if !isEmpty(sequenceDiff) {
change.Sequences = sequenceDiff
hasChanges = true
}
// Compare scripts
scriptDiff := compareScripts(source.Scripts, target.Scripts)
if !isEmpty(scriptDiff) {
change.Scripts = scriptDiff
hasChanges = true
}
if !hasChanges {
return nil
}
return change
}
func compareTables(source, target []*models.Table) *TableDiff {
diff := &TableDiff{
Missing: make([]*models.Table, 0),
Extra: make([]*models.Table, 0),
Modified: make([]*TableChange, 0),
}
sourceMap := make(map[string]*models.Table)
targetMap := make(map[string]*models.Table)
for _, t := range source {
sourceMap[t.SQLName()] = t
}
for _, t := range target {
targetMap[t.SQLName()] = t
}
// Find missing and modified tables
for _, name := range sortedKeys(sourceMap) {
srcTable := sourceMap[name]
if tgtTable, exists := targetMap[name]; !exists {
diff.Missing = append(diff.Missing, srcTable)
} else {
if change := compareTableDetails(srcTable, tgtTable); change != nil {
diff.Modified = append(diff.Modified, change)
}
}
}
// Find extra tables
for _, name := range sortedKeys(targetMap) {
tgtTable := targetMap[name]
if _, exists := sourceMap[name]; !exists {
diff.Extra = append(diff.Extra, tgtTable)
}
}
return diff
}
func compareTableDetails(source, target *models.Table) *TableChange {
change := &TableChange{
Name: source.Name,
Schema: source.Schema,
}
hasChanges := false
// Compare columns
columnDiff := compareColumns(source.Columns, target.Columns)
if !isEmpty(columnDiff) {
change.Columns = columnDiff
hasChanges = true
}
// Compare indexes
indexDiff := compareIndexes(source.Indexes, target.Indexes)
if !isEmpty(indexDiff) {
change.Indexes = indexDiff
hasChanges = true
}
// Compare constraints
constraintDiff := compareConstraints(source.Constraints, target.Constraints)
if !isEmpty(constraintDiff) {
change.Constraints = constraintDiff
hasChanges = true
}
// Compare relationships
relationshipDiff := compareRelationships(source.Relationships, target.Relationships)
if !isEmpty(relationshipDiff) {
change.Relationships = relationshipDiff
hasChanges = true
}
if !hasChanges {
return nil
}
return change
}
func compareColumns(source, target map[string]*models.Column) *ColumnDiff {
diff := &ColumnDiff{
Missing: make([]*models.Column, 0),
Extra: make([]*models.Column, 0),
Modified: make([]*ColumnChange, 0),
}
// Find missing and modified columns
for _, name := range sortedKeys(source) {
srcCol := source[name]
if tgtCol, exists := target[name]; !exists {
diff.Missing = append(diff.Missing, srcCol)
} else {
if changes := compareColumnDetails(srcCol, tgtCol); len(changes) > 0 {
diff.Modified = append(diff.Modified, &ColumnChange{
Name: name,
Source: srcCol,
Target: tgtCol,
Changes: changes,
})
}
}
}
// Find extra columns
for _, name := range sortedKeys(target) {
tgtCol := target[name]
if _, exists := source[name]; !exists {
diff.Extra = append(diff.Extra, tgtCol)
}
}
return diff
}
func compareColumnDetails(source, target *models.Column) map[string]any {
changes := make(map[string]any)
if source.Type != target.Type {
changes["type"] = map[string]string{"source": source.Type, "target": target.Type}
}
if source.Length != target.Length {
changes["length"] = map[string]int{"source": source.Length, "target": target.Length}
}
if source.Precision != target.Precision {
changes["precision"] = map[string]int{"source": source.Precision, "target": target.Precision}
}
if source.Scale != target.Scale {
changes["scale"] = map[string]int{"source": source.Scale, "target": target.Scale}
}
if source.NotNull != target.NotNull {
changes["not_null"] = map[string]bool{"source": source.NotNull, "target": target.NotNull}
}
if !reflect.DeepEqual(source.Default, target.Default) {
changes["default"] = map[string]any{"source": source.Default, "target": target.Default}
}
if source.AutoIncrement != target.AutoIncrement {
changes["auto_increment"] = map[string]bool{"source": source.AutoIncrement, "target": target.AutoIncrement}
}
if source.IsPrimaryKey != target.IsPrimaryKey {
changes["is_primary_key"] = map[string]bool{"source": source.IsPrimaryKey, "target": target.IsPrimaryKey}
}
return changes
}
func compareIndexes(source, target map[string]*models.Index) *IndexDiff {
diff := &IndexDiff{
Missing: make([]*models.Index, 0),
Extra: make([]*models.Index, 0),
Modified: make([]*IndexChange, 0),
}
// Find missing and modified indexes
for _, name := range sortedKeys(source) {
srcIdx := source[name]
if tgtIdx, exists := target[name]; !exists {
diff.Missing = append(diff.Missing, srcIdx)
} else {
if changes := compareIndexDetails(srcIdx, tgtIdx); len(changes) > 0 {
diff.Modified = append(diff.Modified, &IndexChange{
Name: name,
Source: srcIdx,
Target: tgtIdx,
Changes: changes,
})
}
}
}
// Find extra indexes
for _, name := range sortedKeys(target) {
tgtIdx := target[name]
if _, exists := source[name]; !exists {
diff.Extra = append(diff.Extra, tgtIdx)
}
}
return diff
}
func compareIndexDetails(source, target *models.Index) map[string]any {
changes := make(map[string]any)
if !reflect.DeepEqual(source.Columns, target.Columns) {
changes["columns"] = map[string][]string{"source": source.Columns, "target": target.Columns}
}
if source.Unique != target.Unique {
changes["unique"] = map[string]bool{"source": source.Unique, "target": target.Unique}
}
if source.Type != target.Type {
changes["type"] = map[string]string{"source": source.Type, "target": target.Type}
}
if source.Where != target.Where {
changes["where"] = map[string]string{"source": source.Where, "target": target.Where}
}
return changes
}
func compareConstraints(source, target map[string]*models.Constraint) *ConstraintDiff {
diff := &ConstraintDiff{
Missing: make([]*models.Constraint, 0),
Extra: make([]*models.Constraint, 0),
Modified: make([]*ConstraintChange, 0),
}
// Find missing and modified constraints
for _, name := range sortedKeys(source) {
srcCon := source[name]
if tgtCon, exists := target[name]; !exists {
diff.Missing = append(diff.Missing, srcCon)
} else {
if changes := compareConstraintDetails(srcCon, tgtCon); len(changes) > 0 {
diff.Modified = append(diff.Modified, &ConstraintChange{
Name: name,
Source: srcCon,
Target: tgtCon,
Changes: changes,
})
}
}
}
// Find extra constraints
for _, name := range sortedKeys(target) {
tgtCon := target[name]
if _, exists := source[name]; !exists {
diff.Extra = append(diff.Extra, tgtCon)
}
}
return diff
}
func compareConstraintDetails(source, target *models.Constraint) map[string]any {
changes := make(map[string]any)
if source.Type != target.Type {
changes["type"] = map[string]string{"source": string(source.Type), "target": string(target.Type)}
}
if !reflect.DeepEqual(source.Columns, target.Columns) {
changes["columns"] = map[string][]string{"source": source.Columns, "target": target.Columns}
}
if source.ReferencedTable != target.ReferencedTable {
changes["referenced_table"] = map[string]string{"source": source.ReferencedTable, "target": target.ReferencedTable}
}
if !reflect.DeepEqual(source.ReferencedColumns, target.ReferencedColumns) {
changes["referenced_columns"] = map[string][]string{"source": source.ReferencedColumns, "target": target.ReferencedColumns}
}
if source.OnDelete != target.OnDelete {
changes["on_delete"] = map[string]string{"source": source.OnDelete, "target": target.OnDelete}
}
if source.OnUpdate != target.OnUpdate {
changes["on_update"] = map[string]string{"source": source.OnUpdate, "target": target.OnUpdate}
}
return changes
}
func compareRelationships(source, target map[string]*models.Relationship) *RelationshipDiff {
diff := &RelationshipDiff{
Missing: make([]*models.Relationship, 0),
Extra: make([]*models.Relationship, 0),
Modified: make([]*RelationshipChange, 0),
}
// Find missing and modified relationships
for _, name := range sortedKeys(source) {
srcRel := source[name]
if tgtRel, exists := target[name]; !exists {
diff.Missing = append(diff.Missing, srcRel)
} else {
if changes := compareRelationshipDetails(srcRel, tgtRel); len(changes) > 0 {
diff.Modified = append(diff.Modified, &RelationshipChange{
Name: name,
Source: srcRel,
Target: tgtRel,
Changes: changes,
})
}
}
}
// Find extra relationships
for _, name := range sortedKeys(target) {
tgtRel := target[name]
if _, exists := source[name]; !exists {
diff.Extra = append(diff.Extra, tgtRel)
}
}
return diff
}
func compareRelationshipDetails(source, target *models.Relationship) map[string]any {
changes := make(map[string]any)
if source.Type != target.Type {
changes["type"] = map[string]string{"source": string(source.Type), "target": string(target.Type)}
}
if source.FromTable != target.FromTable {
changes["from_table"] = map[string]string{"source": source.FromTable, "target": target.FromTable}
}
if source.ToTable != target.ToTable {
changes["to_table"] = map[string]string{"source": source.ToTable, "target": target.ToTable}
}
if !reflect.DeepEqual(source.FromColumns, target.FromColumns) {
changes["from_columns"] = map[string][]string{"source": source.FromColumns, "target": target.FromColumns}
}
if !reflect.DeepEqual(source.ToColumns, target.ToColumns) {
changes["to_columns"] = map[string][]string{"source": source.ToColumns, "target": target.ToColumns}
}
return changes
}
func compareViews(source, target []*models.View) *ViewDiff {
diff := &ViewDiff{
Missing: make([]*models.View, 0),
Extra: make([]*models.View, 0),
Modified: make([]*ViewChange, 0),
}
sourceMap := make(map[string]*models.View)
targetMap := make(map[string]*models.View)
for _, v := range source {
sourceMap[v.SQLName()] = v
}
for _, v := range target {
targetMap[v.SQLName()] = v
}
// Find missing and modified views
for _, name := range sortedKeys(sourceMap) {
srcView := sourceMap[name]
if tgtView, exists := targetMap[name]; !exists {
diff.Missing = append(diff.Missing, srcView)
} else {
if changes := compareViewDetails(srcView, tgtView); len(changes) > 0 {
diff.Modified = append(diff.Modified, &ViewChange{
Name: name,
Source: srcView,
Target: tgtView,
Changes: changes,
})
}
}
}
// Find extra views
for _, name := range sortedKeys(targetMap) {
tgtView := targetMap[name]
if _, exists := sourceMap[name]; !exists {
diff.Extra = append(diff.Extra, tgtView)
}
}
return diff
}
func compareViewDetails(source, target *models.View) map[string]any {
changes := make(map[string]any)
if source.Definition != target.Definition {
changes["definition"] = map[string]string{"source": source.Definition, "target": target.Definition}
}
return changes
}
func compareSequences(source, target []*models.Sequence) *SequenceDiff {
diff := &SequenceDiff{
Missing: make([]*models.Sequence, 0),
Extra: make([]*models.Sequence, 0),
Modified: make([]*SequenceChange, 0),
}
sourceMap := make(map[string]*models.Sequence)
targetMap := make(map[string]*models.Sequence)
for _, s := range source {
sourceMap[s.SQLName()] = s
}
for _, s := range target {
targetMap[s.SQLName()] = s
}
// Find missing and modified sequences
for _, name := range sortedKeys(sourceMap) {
srcSeq := sourceMap[name]
if tgtSeq, exists := targetMap[name]; !exists {
diff.Missing = append(diff.Missing, srcSeq)
} else {
if changes := compareSequenceDetails(srcSeq, tgtSeq); len(changes) > 0 {
diff.Modified = append(diff.Modified, &SequenceChange{
Name: name,
Source: srcSeq,
Target: tgtSeq,
Changes: changes,
})
}
}
}
// Find extra sequences
for _, name := range sortedKeys(targetMap) {
tgtSeq := targetMap[name]
if _, exists := sourceMap[name]; !exists {
diff.Extra = append(diff.Extra, tgtSeq)
}
}
return diff
}
func compareSequenceDetails(source, target *models.Sequence) map[string]any {
changes := make(map[string]any)
if source.StartValue != target.StartValue {
changes["start_value"] = map[string]int64{"source": source.StartValue, "target": target.StartValue}
}
if source.IncrementBy != target.IncrementBy {
changes["increment_by"] = map[string]int64{"source": source.IncrementBy, "target": target.IncrementBy}
}
if source.MinValue != target.MinValue {
changes["min_value"] = map[string]int64{"source": source.MinValue, "target": target.MinValue}
}
if source.MaxValue != target.MaxValue {
changes["max_value"] = map[string]int64{"source": source.MaxValue, "target": target.MaxValue}
}
if source.Cycle != target.Cycle {
changes["cycle"] = map[string]bool{"source": source.Cycle, "target": target.Cycle}
}
return changes
}
func compareScripts(source, target []*models.Script) *ScriptDiff {
diff := &ScriptDiff{
Missing: make([]*models.Script, 0),
Extra: make([]*models.Script, 0),
Modified: make([]*ScriptChange, 0),
}
sourceMap := make(map[string]*models.Script)
targetMap := make(map[string]*models.Script)
for _, s := range source {
sourceMap[scriptCompareKey(s)] = s
}
for _, s := range target {
targetMap[scriptCompareKey(s)] = s
}
for _, name := range sortedKeys(sourceMap) {
srcScript := sourceMap[name]
if tgtScript, exists := targetMap[name]; !exists {
diff.Missing = append(diff.Missing, srcScript)
} else if changes := compareScriptDetails(srcScript, tgtScript); len(changes) > 0 {
diff.Modified = append(diff.Modified, &ScriptChange{
Name: srcScript.Name,
Source: srcScript,
Target: tgtScript,
Changes: changes,
})
}
}
for _, name := range sortedKeys(targetMap) {
tgtScript := targetMap[name]
if _, exists := sourceMap[name]; !exists {
diff.Extra = append(diff.Extra, tgtScript)
}
}
return diff
}
func scriptCompareKey(script *models.Script) string {
return fmt.Sprintf("%d:%d:%s", script.Priority, script.Sequence, script.SQLName())
}
func compareScriptDetails(source, target *models.Script) map[string]any {
changes := make(map[string]any)
if source.SQL != target.SQL {
changes["sql"] = map[string]string{"source": source.SQL, "target": target.SQL}
}
if source.Rollback != target.Rollback {
changes["rollback"] = map[string]string{"source": source.Rollback, "target": target.Rollback}
}
if !reflect.DeepEqual(source.RunAfter, target.RunAfter) {
changes["run_after"] = map[string][]string{"source": source.RunAfter, "target": target.RunAfter}
}
if source.Schema != target.Schema {
changes["schema"] = map[string]string{"source": source.Schema, "target": target.Schema}
}
if source.Version != target.Version {
changes["version"] = map[string]string{"source": source.Version, "target": target.Version}
}
if source.Priority != target.Priority {
changes["priority"] = map[string]int{"source": source.Priority, "target": target.Priority}
}
if source.Sequence != target.Sequence {
changes["sequence"] = map[string]uint{"source": source.Sequence, "target": target.Sequence}
}
return changes
}
// Helper function to check if a diff is empty
func isEmpty(v any) bool {
switch d := v.(type) {
case *TableDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *ColumnDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *IndexDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *ConstraintDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *RelationshipDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *ViewDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *SequenceDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
case *ScriptDiff:
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
default:
return false
}
}
// ComputeSummary generates a summary with counts from a DiffResult
func ComputeSummary(result *DiffResult) *Summary {
summary := &Summary{}
if result.Schemas != nil {
summary.Schemas = SchemaSummary{
Missing: len(result.Schemas.Missing),
Extra: len(result.Schemas.Extra),
Modified: len(result.Schemas.Modified),
}
// Aggregate table/column/index/constraint counts
for _, schemaChange := range result.Schemas.Modified {
if schemaChange.Tables != nil {
summary.Tables.Missing += len(schemaChange.Tables.Missing)
summary.Tables.Extra += len(schemaChange.Tables.Extra)
summary.Tables.Modified += len(schemaChange.Tables.Modified)
for _, tableChange := range schemaChange.Tables.Modified {
if tableChange.Columns != nil {
summary.Columns.Missing += len(tableChange.Columns.Missing)
summary.Columns.Extra += len(tableChange.Columns.Extra)
summary.Columns.Modified += len(tableChange.Columns.Modified)
}
if tableChange.Indexes != nil {
summary.Indexes.Missing += len(tableChange.Indexes.Missing)
summary.Indexes.Extra += len(tableChange.Indexes.Extra)
summary.Indexes.Modified += len(tableChange.Indexes.Modified)
}
if tableChange.Constraints != nil {
summary.Constraints.Missing += len(tableChange.Constraints.Missing)
summary.Constraints.Extra += len(tableChange.Constraints.Extra)
summary.Constraints.Modified += len(tableChange.Constraints.Modified)
}
if tableChange.Relationships != nil {
summary.Relationships.Missing += len(tableChange.Relationships.Missing)
summary.Relationships.Extra += len(tableChange.Relationships.Extra)
summary.Relationships.Modified += len(tableChange.Relationships.Modified)
}
}
}
if schemaChange.Views != nil {
summary.Views.Missing += len(schemaChange.Views.Missing)
summary.Views.Extra += len(schemaChange.Views.Extra)
summary.Views.Modified += len(schemaChange.Views.Modified)
}
if schemaChange.Sequences != nil {
summary.Sequences.Missing += len(schemaChange.Sequences.Missing)
summary.Sequences.Extra += len(schemaChange.Sequences.Extra)
summary.Sequences.Modified += len(schemaChange.Sequences.Modified)
}
if schemaChange.Scripts != nil {
summary.Scripts.Missing += len(schemaChange.Scripts.Missing)
summary.Scripts.Extra += len(schemaChange.Scripts.Extra)
summary.Scripts.Modified += len(schemaChange.Scripts.Modified)
}
}
}
return summary
}