diff --git a/examples/test_schema_modified.dbml b/examples/test_schema_modified.dbml new file mode 100644 index 0000000..715d491 --- /dev/null +++ b/examples/test_schema_modified.dbml @@ -0,0 +1,31 @@ +// Modified test schema for diff testing +Table public.users { + id bigint [pk, increment] + email varchar(255) [unique, not null] + name varchar(150) // Changed from 100 to 150 + phone varchar(20) // New column + created_at timestamp [not null] + updated_at timestamp +} + +Table public.posts { + id bigint [pk, increment] + user_id bigint [not null] + title varchar(200) [not null] + content text + published boolean [default: true] // Changed default from false to true + views integer [default: 0] // New column + created_at timestamp [not null] +} + +Table public.comments { + id bigint [pk, increment] + post_id bigint [not null] + user_id bigint [not null] + content text [not null] + created_at timestamp [not null] +} + +Ref: public.posts.user_id > public.users.id [ondelete: CASCADE] +Ref: public.comments.post_id > public.posts.id [ondelete: CASCADE] +Ref: public.comments.user_id > public.users.id diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go new file mode 100644 index 0000000..dbb6a55 --- /dev/null +++ b/pkg/diff/diff.go @@ -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 +} diff --git a/pkg/diff/formatters.go b/pkg/diff/formatters.go new file mode 100644 index 0000000..1850dcb --- /dev/null +++ b/pkg/diff/formatters.go @@ -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 = ` + +
+ + +Source: {{.Result.Source}}
+Target: {{.Result.Target}}
+ +