package diff import ( "reflect" "git.warky.dev/wdevs/relspecgo/pkg/models" ) // 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, srcSchema := range sourceMap { 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, tgtSchema := range targetMap { 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 } 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, srcTable := range sourceMap { 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, tgtTable := range targetMap { 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, srcCol := range source { 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, tgtCol := range target { 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, srcIdx := range source { 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, tgtIdx := range target { 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, srcCon := range source { 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, tgtCon := range target { 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, srcRel := range source { 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, tgtRel := range target { 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, srcView := range sourceMap { 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, tgtView := range targetMap { 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, srcSeq := range sourceMap { 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, tgtSeq := range targetMap { 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 } // 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 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) } } } return summary }