Added diff to the tool
This commit is contained in:
594
pkg/diff/diff.go
Normal file
594
pkg/diff/diff.go
Normal file
@@ -0,0 +1,594 @@
|
||||
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
|
||||
}
|
||||
601
pkg/diff/formatters.go
Normal file
601
pkg/diff/formatters.go
Normal file
@@ -0,0 +1,601 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// OutputFormat represents the output format for diff results
|
||||
type OutputFormat string
|
||||
|
||||
const (
|
||||
FormatSummary OutputFormat = "summary"
|
||||
FormatJSON OutputFormat = "json"
|
||||
FormatHTML OutputFormat = "html"
|
||||
)
|
||||
|
||||
// FormatDiff formats the diff result according to the specified format
|
||||
func FormatDiff(result *DiffResult, format OutputFormat, w io.Writer) error {
|
||||
switch format {
|
||||
case FormatSummary:
|
||||
return formatSummary(result, w)
|
||||
case FormatJSON:
|
||||
return formatJSON(result, w)
|
||||
case FormatHTML:
|
||||
return formatHTML(result, w)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
func formatSummary(result *DiffResult, w io.Writer) error {
|
||||
summary := ComputeSummary(result)
|
||||
|
||||
fmt.Fprintf(w, "\n=== Database Diff Summary ===\n")
|
||||
fmt.Fprintf(w, "Source: %s\n", result.Source)
|
||||
fmt.Fprintf(w, "Target: %s\n\n", result.Target)
|
||||
|
||||
// Schemas
|
||||
if summary.Schemas.Missing > 0 || summary.Schemas.Extra > 0 || summary.Schemas.Modified > 0 {
|
||||
fmt.Fprintf(w, "Schemas:\n")
|
||||
if summary.Schemas.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Schemas.Missing)
|
||||
}
|
||||
if summary.Schemas.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Schemas.Extra)
|
||||
}
|
||||
if summary.Schemas.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Schemas.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Tables
|
||||
if summary.Tables.Missing > 0 || summary.Tables.Extra > 0 || summary.Tables.Modified > 0 {
|
||||
fmt.Fprintf(w, "Tables:\n")
|
||||
if summary.Tables.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Tables.Missing)
|
||||
}
|
||||
if summary.Tables.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Tables.Extra)
|
||||
}
|
||||
if summary.Tables.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Tables.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Columns
|
||||
if summary.Columns.Missing > 0 || summary.Columns.Extra > 0 || summary.Columns.Modified > 0 {
|
||||
fmt.Fprintf(w, "Columns:\n")
|
||||
if summary.Columns.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Columns.Missing)
|
||||
}
|
||||
if summary.Columns.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Columns.Extra)
|
||||
}
|
||||
if summary.Columns.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Columns.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Indexes
|
||||
if summary.Indexes.Missing > 0 || summary.Indexes.Extra > 0 || summary.Indexes.Modified > 0 {
|
||||
fmt.Fprintf(w, "Indexes:\n")
|
||||
if summary.Indexes.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Indexes.Missing)
|
||||
}
|
||||
if summary.Indexes.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Indexes.Extra)
|
||||
}
|
||||
if summary.Indexes.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Indexes.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Constraints
|
||||
if summary.Constraints.Missing > 0 || summary.Constraints.Extra > 0 || summary.Constraints.Modified > 0 {
|
||||
fmt.Fprintf(w, "Constraints:\n")
|
||||
if summary.Constraints.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Constraints.Missing)
|
||||
}
|
||||
if summary.Constraints.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Constraints.Extra)
|
||||
}
|
||||
if summary.Constraints.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Constraints.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Relationships
|
||||
if summary.Relationships.Missing > 0 || summary.Relationships.Extra > 0 || summary.Relationships.Modified > 0 {
|
||||
fmt.Fprintf(w, "Relationships:\n")
|
||||
if summary.Relationships.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Relationships.Missing)
|
||||
}
|
||||
if summary.Relationships.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Relationships.Extra)
|
||||
}
|
||||
if summary.Relationships.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Relationships.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Views
|
||||
if summary.Views.Missing > 0 || summary.Views.Extra > 0 || summary.Views.Modified > 0 {
|
||||
fmt.Fprintf(w, "Views:\n")
|
||||
if summary.Views.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Views.Missing)
|
||||
}
|
||||
if summary.Views.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Views.Extra)
|
||||
}
|
||||
if summary.Views.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Views.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Sequences
|
||||
if summary.Sequences.Missing > 0 || summary.Sequences.Extra > 0 || summary.Sequences.Modified > 0 {
|
||||
fmt.Fprintf(w, "Sequences:\n")
|
||||
if summary.Sequences.Missing > 0 {
|
||||
fmt.Fprintf(w, " Missing: %d\n", summary.Sequences.Missing)
|
||||
}
|
||||
if summary.Sequences.Extra > 0 {
|
||||
fmt.Fprintf(w, " Extra: %d\n", summary.Sequences.Extra)
|
||||
}
|
||||
if summary.Sequences.Modified > 0 {
|
||||
fmt.Fprintf(w, " Modified: %d\n", summary.Sequences.Modified)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
|
||||
// Check if there are no differences
|
||||
if summary.Schemas.Missing == 0 && summary.Schemas.Extra == 0 && summary.Schemas.Modified == 0 &&
|
||||
summary.Tables.Missing == 0 && summary.Tables.Extra == 0 && summary.Tables.Modified == 0 &&
|
||||
summary.Columns.Missing == 0 && summary.Columns.Extra == 0 && summary.Columns.Modified == 0 &&
|
||||
summary.Indexes.Missing == 0 && summary.Indexes.Extra == 0 && summary.Indexes.Modified == 0 &&
|
||||
summary.Constraints.Missing == 0 && summary.Constraints.Extra == 0 && summary.Constraints.Modified == 0 &&
|
||||
summary.Relationships.Missing == 0 && summary.Relationships.Extra == 0 && summary.Relationships.Modified == 0 &&
|
||||
summary.Views.Missing == 0 && summary.Views.Extra == 0 && summary.Views.Modified == 0 &&
|
||||
summary.Sequences.Missing == 0 && summary.Sequences.Extra == 0 && summary.Sequences.Modified == 0 {
|
||||
fmt.Fprintf(w, "No differences found.\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatJSON(result *DiffResult, w io.Writer) error {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(result)
|
||||
}
|
||||
|
||||
func formatHTML(result *DiffResult, w io.Writer) error {
|
||||
tmpl, err := template.New("diff").Funcs(template.FuncMap{
|
||||
"join": strings.Join,
|
||||
}).Parse(htmlTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
|
||||
summary := ComputeSummary(result)
|
||||
data := struct {
|
||||
Result *DiffResult
|
||||
Summary *Summary
|
||||
}{
|
||||
Result: result,
|
||||
Summary: summary,
|
||||
}
|
||||
|
||||
return tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
const htmlTemplate = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Database Diff Report</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #333;
|
||||
}
|
||||
.summary {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.summary-item {
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.summary-item h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: #666;
|
||||
}
|
||||
.count-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.count {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.count-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.count-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.missing { color: #dc3545; }
|
||||
.extra { color: #28a745; }
|
||||
.modified { color: #ffc107; }
|
||||
.details {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.schema-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.table-section {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.item-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.item-list li {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #ccc;
|
||||
}
|
||||
.item-list li.missing {
|
||||
border-left-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
.item-list li.extra {
|
||||
border-left-color: #28a745;
|
||||
background: #f0fff4;
|
||||
}
|
||||
.item-list li.modified {
|
||||
border-left-color: #ffc107;
|
||||
background: #fffbf0;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.badge.missing { background: #dc3545; color: white; }
|
||||
.badge.extra { background: #28a745; color: white; }
|
||||
.badge.modified { background: #ffc107; color: #333; }
|
||||
.no-diff {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #28a745;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Database Diff Report</h1>
|
||||
|
||||
<div class="summary">
|
||||
<h2>Summary</h2>
|
||||
<p><strong>Source:</strong> {{.Result.Source}}</p>
|
||||
<p><strong>Target:</strong> {{.Result.Target}}</p>
|
||||
|
||||
<div class="summary-grid">
|
||||
{{if or .Summary.Schemas.Missing .Summary.Schemas.Extra .Summary.Schemas.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Schemas</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Schemas.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Schemas.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Schemas.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Summary.Tables.Missing .Summary.Tables.Extra .Summary.Tables.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Tables</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Tables.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Tables.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Tables.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Summary.Columns.Missing .Summary.Columns.Extra .Summary.Columns.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Columns</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Columns.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Columns.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Columns.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Summary.Indexes.Missing .Summary.Indexes.Extra .Summary.Indexes.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Indexes</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Indexes.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Indexes.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Indexes.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Summary.Constraints.Missing .Summary.Constraints.Extra .Summary.Constraints.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Constraints</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Constraints.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Constraints.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Constraints.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Summary.Sequences.Missing .Summary.Sequences.Extra .Summary.Sequences.Modified}}
|
||||
<div class="summary-item">
|
||||
<h3>Sequences</h3>
|
||||
<div class="count-group">
|
||||
<div class="count">
|
||||
<span class="count-label">Missing</span>
|
||||
<span class="count-value missing">{{.Summary.Sequences.Missing}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Extra</span>
|
||||
<span class="count-value extra">{{.Summary.Sequences.Extra}}</span>
|
||||
</div>
|
||||
<div class="count">
|
||||
<span class="count-label">Modified</span>
|
||||
<span class="count-value modified">{{.Summary.Sequences.Modified}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Result.Schemas}}
|
||||
<div class="details">
|
||||
<h2>Detailed Differences</h2>
|
||||
|
||||
{{range .Result.Schemas.Missing}}
|
||||
<div class="schema-section">
|
||||
<h3>Schema: {{.Name}} <span class="badge missing">MISSING</span></h3>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{range .Result.Schemas.Extra}}
|
||||
<div class="schema-section">
|
||||
<h3>Schema: {{.Name}} <span class="badge extra">EXTRA</span></h3>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{range .Result.Schemas.Modified}}
|
||||
<div class="schema-section">
|
||||
<h3>Schema: {{.Name}} <span class="badge modified">MODIFIED</span></h3>
|
||||
|
||||
{{if .Tables}}
|
||||
{{if .Tables.Missing}}
|
||||
<h4>Missing Tables</h4>
|
||||
<ul class="item-list">
|
||||
{{range .Tables.Missing}}
|
||||
<li class="missing">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Tables.Extra}}
|
||||
<h4>Extra Tables</h4>
|
||||
<ul class="item-list">
|
||||
{{range .Tables.Extra}}
|
||||
<li class="extra">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Tables.Modified}}
|
||||
<h4>Modified Tables</h4>
|
||||
{{range .Tables.Modified}}
|
||||
<div class="table-section">
|
||||
<h5>Table: {{.Schema}}.{{.Name}}</h5>
|
||||
|
||||
{{if .Columns}}
|
||||
{{if .Columns.Missing}}
|
||||
<h6>Missing Columns</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Columns.Missing}}
|
||||
<li class="missing">{{.Name}} ({{.Type}})</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Columns.Extra}}
|
||||
<h6>Extra Columns</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Columns.Extra}}
|
||||
<li class="extra">{{.Name}} ({{.Type}})</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Columns.Modified}}
|
||||
<h6>Modified Columns</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Columns.Modified}}
|
||||
<li class="modified">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Indexes}}
|
||||
{{if .Indexes.Missing}}
|
||||
<h6>Missing Indexes</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Indexes.Missing}}
|
||||
<li class="missing">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Indexes.Extra}}
|
||||
<h6>Extra Indexes</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Indexes.Extra}}
|
||||
<li class="extra">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Constraints}}
|
||||
{{if .Constraints.Missing}}
|
||||
<h6>Missing Constraints</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Constraints.Missing}}
|
||||
<li class="missing">{{.Name}} ({{.Type}})</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Constraints.Extra}}
|
||||
<h6>Extra Constraints</h6>
|
||||
<ul class="item-list">
|
||||
{{range .Constraints.Extra}}
|
||||
<li class="extra">{{.Name}} ({{.Type}})</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Sequences}}
|
||||
{{if .Sequences.Missing}}
|
||||
<h4>Missing Sequences</h4>
|
||||
<ul class="item-list">
|
||||
{{range .Sequences.Missing}}
|
||||
<li class="missing">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{if .Sequences.Extra}}
|
||||
<h4>Extra Sequences</h4>
|
||||
<ul class="item-list">
|
||||
{{range .Sequences.Extra}}
|
||||
<li class="extra">{{.Name}}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-diff">
|
||||
✓ No differences found between the databases
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
192
pkg/diff/types.go
Normal file
192
pkg/diff/types.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package diff
|
||||
|
||||
import "git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
|
||||
// DiffResult represents the complete difference analysis between two databases
|
||||
type DiffResult struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Schemas *SchemaDiff `json:"schemas"`
|
||||
}
|
||||
|
||||
// SchemaDiff represents differences at the schema level
|
||||
type SchemaDiff struct {
|
||||
Missing []*models.Schema `json:"missing"` // Schemas in source but not in target
|
||||
Extra []*models.Schema `json:"extra"` // Schemas in target but not in source
|
||||
Modified []*SchemaChange `json:"modified"` // Schemas that exist in both but differ
|
||||
}
|
||||
|
||||
// SchemaChange represents changes within a schema
|
||||
type SchemaChange struct {
|
||||
Name string `json:"name"`
|
||||
Tables *TableDiff `json:"tables,omitempty"`
|
||||
Views *ViewDiff `json:"views,omitempty"`
|
||||
Sequences *SequenceDiff `json:"sequences,omitempty"`
|
||||
}
|
||||
|
||||
// TableDiff represents differences in tables
|
||||
type TableDiff struct {
|
||||
Missing []*models.Table `json:"missing"` // Tables in source but not in target
|
||||
Extra []*models.Table `json:"extra"` // Tables in target but not in source
|
||||
Modified []*TableChange `json:"modified"` // Tables that exist in both but differ
|
||||
}
|
||||
|
||||
// TableChange represents changes within a table
|
||||
type TableChange struct {
|
||||
Name string `json:"name"`
|
||||
Schema string `json:"schema"`
|
||||
Columns *ColumnDiff `json:"columns,omitempty"`
|
||||
Indexes *IndexDiff `json:"indexes,omitempty"`
|
||||
Constraints *ConstraintDiff `json:"constraints,omitempty"`
|
||||
Relationships *RelationshipDiff `json:"relationships,omitempty"`
|
||||
}
|
||||
|
||||
// ColumnDiff represents differences in columns
|
||||
type ColumnDiff struct {
|
||||
Missing []*models.Column `json:"missing"` // Columns in source but not in target
|
||||
Extra []*models.Column `json:"extra"` // Columns in target but not in source
|
||||
Modified []*ColumnChange `json:"modified"` // Columns that exist in both but differ
|
||||
}
|
||||
|
||||
// ColumnChange represents a modified column
|
||||
type ColumnChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.Column `json:"source"`
|
||||
Target *models.Column `json:"target"`
|
||||
Changes map[string]any `json:"changes"` // Map of field name to what changed
|
||||
}
|
||||
|
||||
// IndexDiff represents differences in indexes
|
||||
type IndexDiff struct {
|
||||
Missing []*models.Index `json:"missing"` // Indexes in source but not in target
|
||||
Extra []*models.Index `json:"extra"` // Indexes in target but not in source
|
||||
Modified []*IndexChange `json:"modified"` // Indexes that exist in both but differ
|
||||
}
|
||||
|
||||
// IndexChange represents a modified index
|
||||
type IndexChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.Index `json:"source"`
|
||||
Target *models.Index `json:"target"`
|
||||
Changes map[string]any `json:"changes"`
|
||||
}
|
||||
|
||||
// ConstraintDiff represents differences in constraints
|
||||
type ConstraintDiff struct {
|
||||
Missing []*models.Constraint `json:"missing"` // Constraints in source but not in target
|
||||
Extra []*models.Constraint `json:"extra"` // Constraints in target but not in source
|
||||
Modified []*ConstraintChange `json:"modified"` // Constraints that exist in both but differ
|
||||
}
|
||||
|
||||
// ConstraintChange represents a modified constraint
|
||||
type ConstraintChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.Constraint `json:"source"`
|
||||
Target *models.Constraint `json:"target"`
|
||||
Changes map[string]any `json:"changes"`
|
||||
}
|
||||
|
||||
// RelationshipDiff represents differences in relationships
|
||||
type RelationshipDiff struct {
|
||||
Missing []*models.Relationship `json:"missing"` // Relationships in source but not in target
|
||||
Extra []*models.Relationship `json:"extra"` // Relationships in target but not in source
|
||||
Modified []*RelationshipChange `json:"modified"` // Relationships that exist in both but differ
|
||||
}
|
||||
|
||||
// RelationshipChange represents a modified relationship
|
||||
type RelationshipChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.Relationship `json:"source"`
|
||||
Target *models.Relationship `json:"target"`
|
||||
Changes map[string]any `json:"changes"`
|
||||
}
|
||||
|
||||
// ViewDiff represents differences in views
|
||||
type ViewDiff struct {
|
||||
Missing []*models.View `json:"missing"` // Views in source but not in target
|
||||
Extra []*models.View `json:"extra"` // Views in target but not in source
|
||||
Modified []*ViewChange `json:"modified"` // Views that exist in both but differ
|
||||
}
|
||||
|
||||
// ViewChange represents a modified view
|
||||
type ViewChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.View `json:"source"`
|
||||
Target *models.View `json:"target"`
|
||||
Changes map[string]any `json:"changes"`
|
||||
}
|
||||
|
||||
// SequenceDiff represents differences in sequences
|
||||
type SequenceDiff struct {
|
||||
Missing []*models.Sequence `json:"missing"` // Sequences in source but not in target
|
||||
Extra []*models.Sequence `json:"extra"` // Sequences in target but not in source
|
||||
Modified []*SequenceChange `json:"modified"` // Sequences that exist in both but differ
|
||||
}
|
||||
|
||||
// SequenceChange represents a modified sequence
|
||||
type SequenceChange struct {
|
||||
Name string `json:"name"`
|
||||
Source *models.Sequence `json:"source"`
|
||||
Target *models.Sequence `json:"target"`
|
||||
Changes map[string]any `json:"changes"`
|
||||
}
|
||||
|
||||
// Summary provides counts for quick overview
|
||||
type Summary struct {
|
||||
Schemas SchemaSummary `json:"schemas"`
|
||||
Tables TableSummary `json:"tables"`
|
||||
Columns ColumnSummary `json:"columns"`
|
||||
Indexes IndexSummary `json:"indexes"`
|
||||
Constraints ConstraintSummary `json:"constraints"`
|
||||
Relationships RelationshipSummary `json:"relationships"`
|
||||
Views ViewSummary `json:"views"`
|
||||
Sequences SequenceSummary `json:"sequences"`
|
||||
}
|
||||
|
||||
type SchemaSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type TableSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type ColumnSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type IndexSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type ConstraintSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type RelationshipSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type ViewSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
|
||||
type SequenceSummary struct {
|
||||
Missing int `json:"missing"`
|
||||
Extra int `json:"extra"`
|
||||
Modified int `json:"modified"`
|
||||
}
|
||||
Reference in New Issue
Block a user