602 lines
20 KiB
Go
602 lines
20 KiB
Go
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>
|
|
`
|