840 lines
26 KiB
Go
840 lines
26 KiB
Go
package diff
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
)
|
|
|
|
// sortedKeys returns a map's keys sorted alphabetically, so callers get a
|
|
// deterministic iteration order instead of Go's randomized map order.
|
|
func sortedKeys[T any](m map[string]T) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// CompareDatabases compares two database models and returns the differences
|
|
func CompareDatabases(source, target *models.Database) *DiffResult {
|
|
result := &DiffResult{
|
|
Source: source.Name,
|
|
Target: target.Name,
|
|
Schemas: compareSchemas(source.Schemas, target.Schemas),
|
|
}
|
|
return result
|
|
}
|
|
|
|
func compareSchemas(source, target []*models.Schema) *SchemaDiff {
|
|
diff := &SchemaDiff{
|
|
Missing: make([]*models.Schema, 0),
|
|
Extra: make([]*models.Schema, 0),
|
|
Modified: make([]*SchemaChange, 0),
|
|
}
|
|
|
|
sourceMap := make(map[string]*models.Schema)
|
|
targetMap := make(map[string]*models.Schema)
|
|
|
|
for _, s := range source {
|
|
sourceMap[s.SQLName()] = s
|
|
}
|
|
for _, t := range target {
|
|
targetMap[t.SQLName()] = t
|
|
}
|
|
|
|
// Find missing and modified schemas
|
|
for _, name := range sortedKeys(sourceMap) {
|
|
srcSchema := sourceMap[name]
|
|
if tgtSchema, exists := targetMap[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcSchema)
|
|
} else {
|
|
if change := compareSchemaDetails(srcSchema, tgtSchema); change != nil {
|
|
diff.Modified = append(diff.Modified, change)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra schemas
|
|
for _, name := range sortedKeys(targetMap) {
|
|
tgtSchema := targetMap[name]
|
|
if _, exists := sourceMap[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtSchema)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareSchemaDetails(source, target *models.Schema) *SchemaChange {
|
|
change := &SchemaChange{
|
|
Name: source.Name,
|
|
}
|
|
|
|
hasChanges := false
|
|
|
|
// Compare tables
|
|
tableDiff := compareTables(source.Tables, target.Tables)
|
|
if !isEmpty(tableDiff) {
|
|
change.Tables = tableDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare views
|
|
viewDiff := compareViews(source.Views, target.Views)
|
|
if !isEmpty(viewDiff) {
|
|
change.Views = viewDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare sequences
|
|
sequenceDiff := compareSequences(source.Sequences, target.Sequences)
|
|
if !isEmpty(sequenceDiff) {
|
|
change.Sequences = sequenceDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare scripts
|
|
scriptDiff := compareScripts(source.Scripts, target.Scripts)
|
|
if !isEmpty(scriptDiff) {
|
|
change.Scripts = scriptDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
if !hasChanges {
|
|
return nil
|
|
}
|
|
return change
|
|
}
|
|
|
|
func compareTables(source, target []*models.Table) *TableDiff {
|
|
diff := &TableDiff{
|
|
Missing: make([]*models.Table, 0),
|
|
Extra: make([]*models.Table, 0),
|
|
Modified: make([]*TableChange, 0),
|
|
}
|
|
|
|
sourceMap := make(map[string]*models.Table)
|
|
targetMap := make(map[string]*models.Table)
|
|
|
|
for _, t := range source {
|
|
sourceMap[t.SQLName()] = t
|
|
}
|
|
for _, t := range target {
|
|
targetMap[t.SQLName()] = t
|
|
}
|
|
|
|
// Find missing and modified tables
|
|
for _, name := range sortedKeys(sourceMap) {
|
|
srcTable := sourceMap[name]
|
|
if tgtTable, exists := targetMap[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcTable)
|
|
} else {
|
|
if change := compareTableDetails(srcTable, tgtTable); change != nil {
|
|
diff.Modified = append(diff.Modified, change)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra tables
|
|
for _, name := range sortedKeys(targetMap) {
|
|
tgtTable := targetMap[name]
|
|
if _, exists := sourceMap[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtTable)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareTableDetails(source, target *models.Table) *TableChange {
|
|
change := &TableChange{
|
|
Name: source.Name,
|
|
Schema: source.Schema,
|
|
}
|
|
|
|
hasChanges := false
|
|
|
|
// Compare columns
|
|
columnDiff := compareColumns(source.Columns, target.Columns)
|
|
if !isEmpty(columnDiff) {
|
|
change.Columns = columnDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare indexes
|
|
indexDiff := compareIndexes(source.Indexes, target.Indexes)
|
|
if !isEmpty(indexDiff) {
|
|
change.Indexes = indexDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare constraints
|
|
constraintDiff := compareConstraints(source.Constraints, target.Constraints)
|
|
if !isEmpty(constraintDiff) {
|
|
change.Constraints = constraintDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
// Compare relationships
|
|
relationshipDiff := compareRelationships(source.Relationships, target.Relationships)
|
|
if !isEmpty(relationshipDiff) {
|
|
change.Relationships = relationshipDiff
|
|
hasChanges = true
|
|
}
|
|
|
|
if !hasChanges {
|
|
return nil
|
|
}
|
|
return change
|
|
}
|
|
|
|
func compareColumns(source, target map[string]*models.Column) *ColumnDiff {
|
|
diff := &ColumnDiff{
|
|
Missing: make([]*models.Column, 0),
|
|
Extra: make([]*models.Column, 0),
|
|
Modified: make([]*ColumnChange, 0),
|
|
}
|
|
|
|
// Find missing and modified columns
|
|
for _, name := range sortedKeys(source) {
|
|
srcCol := source[name]
|
|
if tgtCol, exists := target[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcCol)
|
|
} else {
|
|
if changes := compareColumnDetails(srcCol, tgtCol); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &ColumnChange{
|
|
Name: name,
|
|
Source: srcCol,
|
|
Target: tgtCol,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra columns
|
|
for _, name := range sortedKeys(target) {
|
|
tgtCol := target[name]
|
|
if _, exists := source[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtCol)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareColumnDetails(source, target *models.Column) map[string]any {
|
|
changes := make(map[string]any)
|
|
sourceType, sourceLength, sourceDefault := comparableColumn(source)
|
|
targetType, targetLength, targetDefault := comparableColumn(target)
|
|
|
|
if sourceType != targetType {
|
|
changes["type"] = map[string]string{"source": source.Type, "target": target.Type}
|
|
}
|
|
if sourceLength != targetLength {
|
|
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(sourceDefault, targetDefault) {
|
|
changes["default"] = map[string]any{"source": sourceDefault, "target": targetDefault}
|
|
}
|
|
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
|
|
}
|
|
|
|
// comparableColumn accepts DBML's compact type/default spelling as well as
|
|
// PostgreSQL's normalized fields (for example varchar(255) vs varchar + 255).
|
|
func comparableColumn(column *models.Column) (string, int, any) {
|
|
typeName := strings.TrimSpace(column.Type)
|
|
defaultValue := column.Default
|
|
lower := strings.ToLower(typeName)
|
|
if i := strings.Index(lower, " default "); i >= 0 {
|
|
if defaultValue == nil {
|
|
defaultValue = strings.TrimSpace(typeName[i+len(" default "):])
|
|
}
|
|
typeName = strings.TrimSpace(typeName[:i])
|
|
}
|
|
length := column.Length
|
|
if open := strings.LastIndex(typeName, "("); open >= 0 && strings.HasSuffix(typeName, ")") {
|
|
if parsed, err := strconv.Atoi(strings.TrimSpace(typeName[open+1 : len(typeName)-1])); err == nil && length == 0 {
|
|
length = parsed
|
|
}
|
|
typeName = strings.TrimSpace(typeName[:open])
|
|
}
|
|
return strings.ToLower(typeName), length, defaultValue
|
|
}
|
|
|
|
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),
|
|
}
|
|
|
|
// Match by name first, then by definition. PostgreSQL and DBML can assign
|
|
// different names to the same index (for example, posts_user_id_title_idx
|
|
// and uidx_posts_user_id_title), so a name-only comparison reports false
|
|
// drift after a merge/diff round trip.
|
|
unmatchedSource := make(map[string]*models.Index, len(source))
|
|
unmatchedTarget := make(map[string]*models.Index, len(target))
|
|
for name, index := range source {
|
|
unmatchedSource[name] = index
|
|
}
|
|
for name, index := range target {
|
|
unmatchedTarget[name] = index
|
|
}
|
|
|
|
for _, name := range sortedKeys(source) {
|
|
srcIdx := source[name]
|
|
tgtIdx, exists := target[name]
|
|
if !exists {
|
|
continue
|
|
}
|
|
delete(unmatchedSource, name)
|
|
delete(unmatchedTarget, name)
|
|
if changes := compareIndexDetails(srcIdx, tgtIdx); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &IndexChange{
|
|
Name: name,
|
|
Source: srcIdx,
|
|
Target: tgtIdx,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Pair remaining indexes by their structural identity, independent of the
|
|
// generated/name field. The sorted iteration makes ambiguous matches
|
|
// deterministic; duplicate definitions are still represented as separate
|
|
// indexes by consuming one target at a time.
|
|
remainingTarget := make(map[string][]*models.Index)
|
|
for _, name := range sortedKeys(unmatchedTarget) {
|
|
index := unmatchedTarget[name]
|
|
key := indexDefinitionKey(index)
|
|
remainingTarget[key] = append(remainingTarget[key], index)
|
|
}
|
|
for _, name := range sortedKeys(unmatchedSource) {
|
|
srcIdx := unmatchedSource[name]
|
|
key := indexDefinitionKey(srcIdx)
|
|
candidates := remainingTarget[key]
|
|
if len(candidates) == 0 {
|
|
diff.Missing = append(diff.Missing, srcIdx)
|
|
continue
|
|
}
|
|
tgtIdx := candidates[0]
|
|
remainingTarget[key] = candidates[1:]
|
|
if changes := compareIndexDetails(srcIdx, tgtIdx); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &IndexChange{
|
|
Name: srcIdx.Name,
|
|
Source: srcIdx,
|
|
Target: tgtIdx,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, key := range sortedKeys(remainingTarget) {
|
|
for _, index := range remainingTarget[key] {
|
|
diff.Extra = append(diff.Extra, index)
|
|
}
|
|
}
|
|
return diff
|
|
}
|
|
|
|
func indexDefinitionKey(index *models.Index) string {
|
|
return fmt.Sprintf("%t:%s:%s", index.Unique, strings.Join(index.Columns, ","), strings.Join(index.Include, ","))
|
|
}
|
|
|
|
func comparableIndexType(indexType string) string {
|
|
indexType = strings.ToLower(strings.TrimSpace(indexType))
|
|
if indexType == "" {
|
|
return "btree"
|
|
}
|
|
return indexType
|
|
}
|
|
|
|
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 comparableIndexType(source.Type) != comparableIndexType(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
|
|
}
|
|
|
|
// Compare constraints.
|
|
// Primary-key constraints are excluded: a PK is already represented by the
|
|
// column's IsPrimaryKey flag, which compareColumns already compares. The
|
|
// PostgreSQL reader additionally materialises each PK as a primary_key
|
|
// constraint and a unique btree index; the DBML reader keeps PKs as column
|
|
// flags only. Comparing the constraint maps directly would therefore report
|
|
// every PK as an "extra" constraint and the generated index as an "extra"
|
|
// index on a freshly-applied schema. Filtering them here keeps the round
|
|
// trip stable without losing real PK information.
|
|
func compareConstraints(source, target map[string]*models.Constraint) *ConstraintDiff {
|
|
filteredSource := filterPrimaryKeyConstraints(source)
|
|
filteredTarget := filterPrimaryKeyConstraints(target)
|
|
sourceByKey := make(map[string]*models.Constraint, len(filteredSource))
|
|
targetByKey := make(map[string]*models.Constraint, len(filteredTarget))
|
|
for _, constraint := range filteredSource {
|
|
sourceByKey[constraintCompareKey(constraint)] = constraint
|
|
}
|
|
for _, constraint := range filteredTarget {
|
|
targetByKey[constraintCompareKey(constraint)] = constraint
|
|
}
|
|
diff := &ConstraintDiff{
|
|
Missing: make([]*models.Constraint, 0),
|
|
Extra: make([]*models.Constraint, 0),
|
|
Modified: make([]*ConstraintChange, 0),
|
|
}
|
|
|
|
// Find missing and modified constraints
|
|
for _, name := range sortedKeys(sourceByKey) {
|
|
srcCon := sourceByKey[name]
|
|
if tgtCon, exists := targetByKey[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcCon)
|
|
} else {
|
|
if changes := compareConstraintDetails(srcCon, tgtCon); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &ConstraintChange{
|
|
Name: name,
|
|
Source: srcCon,
|
|
Target: tgtCon,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra constraints
|
|
for _, name := range sortedKeys(targetByKey) {
|
|
tgtCon := targetByKey[name]
|
|
if _, exists := sourceByKey[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtCon)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
// filterPrimaryKeyConstraints drops primary_key constraints from a single
|
|
// map. Primary keys are compared by the column IsPrimaryKey flag in
|
|
// compareColumns, so comparing the primary_key constraints here only
|
|
// produces duplicate "extra" entries (every PK is extra on the DBML side).
|
|
// Other constraint types are preserved untouched.
|
|
func filterPrimaryKeyConstraints(m map[string]*models.Constraint) map[string]*models.Constraint {
|
|
out := make(map[string]*models.Constraint, len(m))
|
|
for name, c := range m {
|
|
if c.Type == models.PrimaryKeyConstraint {
|
|
continue
|
|
}
|
|
out[name] = c
|
|
}
|
|
return out
|
|
}
|
|
|
|
func constraintCompareKey(constraint *models.Constraint) string {
|
|
if constraint.Type != models.ForeignKeyConstraint {
|
|
return constraint.SQLName()
|
|
}
|
|
return fmt.Sprintf("fk:%s:%s:%s:%s:%s:%s", strings.ToLower(constraint.Schema), strings.ToLower(constraint.Table), strings.Join(constraint.Columns, ","), strings.ToLower(constraint.ReferencedSchema), strings.ToLower(constraint.ReferencedTable), strings.Join(constraint.ReferencedColumns, ","))
|
|
}
|
|
|
|
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 normalizeConstraintAction(source.OnDelete) != normalizeConstraintAction(target.OnDelete) {
|
|
changes["on_delete"] = map[string]string{"source": source.OnDelete, "target": target.OnDelete}
|
|
}
|
|
if normalizeConstraintAction(source.OnUpdate) != normalizeConstraintAction(target.OnUpdate) {
|
|
changes["on_update"] = map[string]string{"source": source.OnUpdate, "target": target.OnUpdate}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
func normalizeConstraintAction(action string) string {
|
|
if strings.EqualFold(strings.TrimSpace(action), "NO ACTION") {
|
|
return ""
|
|
}
|
|
return strings.ToUpper(strings.TrimSpace(action))
|
|
}
|
|
|
|
func compareRelationships(source, target map[string]*models.Relationship) *RelationshipDiff {
|
|
diff := &RelationshipDiff{
|
|
Missing: make([]*models.Relationship, 0),
|
|
Extra: make([]*models.Relationship, 0),
|
|
Modified: make([]*RelationshipChange, 0),
|
|
}
|
|
|
|
// Find missing and modified relationships
|
|
for _, name := range sortedKeys(source) {
|
|
srcRel := source[name]
|
|
if tgtRel, exists := target[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcRel)
|
|
} else {
|
|
if changes := compareRelationshipDetails(srcRel, tgtRel); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &RelationshipChange{
|
|
Name: name,
|
|
Source: srcRel,
|
|
Target: tgtRel,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra relationships
|
|
for _, name := range sortedKeys(target) {
|
|
tgtRel := target[name]
|
|
if _, exists := source[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtRel)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareRelationshipDetails(source, target *models.Relationship) map[string]any {
|
|
changes := make(map[string]any)
|
|
|
|
if source.Type != target.Type {
|
|
changes["type"] = map[string]string{"source": string(source.Type), "target": string(target.Type)}
|
|
}
|
|
if source.FromTable != target.FromTable {
|
|
changes["from_table"] = map[string]string{"source": source.FromTable, "target": target.FromTable}
|
|
}
|
|
if source.ToTable != target.ToTable {
|
|
changes["to_table"] = map[string]string{"source": source.ToTable, "target": target.ToTable}
|
|
}
|
|
if !reflect.DeepEqual(source.FromColumns, target.FromColumns) {
|
|
changes["from_columns"] = map[string][]string{"source": source.FromColumns, "target": target.FromColumns}
|
|
}
|
|
if !reflect.DeepEqual(source.ToColumns, target.ToColumns) {
|
|
changes["to_columns"] = map[string][]string{"source": source.ToColumns, "target": target.ToColumns}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
func compareViews(source, target []*models.View) *ViewDiff {
|
|
diff := &ViewDiff{
|
|
Missing: make([]*models.View, 0),
|
|
Extra: make([]*models.View, 0),
|
|
Modified: make([]*ViewChange, 0),
|
|
}
|
|
|
|
sourceMap := make(map[string]*models.View)
|
|
targetMap := make(map[string]*models.View)
|
|
|
|
for _, v := range source {
|
|
sourceMap[v.SQLName()] = v
|
|
}
|
|
for _, v := range target {
|
|
targetMap[v.SQLName()] = v
|
|
}
|
|
|
|
// Find missing and modified views
|
|
for _, name := range sortedKeys(sourceMap) {
|
|
srcView := sourceMap[name]
|
|
if tgtView, exists := targetMap[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcView)
|
|
} else {
|
|
if changes := compareViewDetails(srcView, tgtView); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &ViewChange{
|
|
Name: name,
|
|
Source: srcView,
|
|
Target: tgtView,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra views
|
|
for _, name := range sortedKeys(targetMap) {
|
|
tgtView := targetMap[name]
|
|
if _, exists := sourceMap[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtView)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareViewDetails(source, target *models.View) map[string]any {
|
|
changes := make(map[string]any)
|
|
|
|
if source.Definition != target.Definition {
|
|
changes["definition"] = map[string]string{"source": source.Definition, "target": target.Definition}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
func compareSequences(source, target []*models.Sequence) *SequenceDiff {
|
|
diff := &SequenceDiff{
|
|
Missing: make([]*models.Sequence, 0),
|
|
Extra: make([]*models.Sequence, 0),
|
|
Modified: make([]*SequenceChange, 0),
|
|
}
|
|
|
|
sourceMap := make(map[string]*models.Sequence)
|
|
targetMap := make(map[string]*models.Sequence)
|
|
|
|
for _, s := range source {
|
|
sourceMap[s.SQLName()] = s
|
|
}
|
|
for _, s := range target {
|
|
targetMap[s.SQLName()] = s
|
|
}
|
|
|
|
// Find missing and modified sequences
|
|
for _, name := range sortedKeys(sourceMap) {
|
|
srcSeq := sourceMap[name]
|
|
if tgtSeq, exists := targetMap[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcSeq)
|
|
} else {
|
|
if changes := compareSequenceDetails(srcSeq, tgtSeq); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &SequenceChange{
|
|
Name: name,
|
|
Source: srcSeq,
|
|
Target: tgtSeq,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find extra sequences
|
|
for _, name := range sortedKeys(targetMap) {
|
|
tgtSeq := targetMap[name]
|
|
if _, exists := sourceMap[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtSeq)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func compareSequenceDetails(source, target *models.Sequence) map[string]any {
|
|
changes := make(map[string]any)
|
|
|
|
if source.StartValue != target.StartValue {
|
|
changes["start_value"] = map[string]int64{"source": source.StartValue, "target": target.StartValue}
|
|
}
|
|
if source.IncrementBy != target.IncrementBy {
|
|
changes["increment_by"] = map[string]int64{"source": source.IncrementBy, "target": target.IncrementBy}
|
|
}
|
|
if source.MinValue != target.MinValue {
|
|
changes["min_value"] = map[string]int64{"source": source.MinValue, "target": target.MinValue}
|
|
}
|
|
if source.MaxValue != target.MaxValue {
|
|
changes["max_value"] = map[string]int64{"source": source.MaxValue, "target": target.MaxValue}
|
|
}
|
|
if source.Cycle != target.Cycle {
|
|
changes["cycle"] = map[string]bool{"source": source.Cycle, "target": target.Cycle}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
func compareScripts(source, target []*models.Script) *ScriptDiff {
|
|
diff := &ScriptDiff{
|
|
Missing: make([]*models.Script, 0),
|
|
Extra: make([]*models.Script, 0),
|
|
Modified: make([]*ScriptChange, 0),
|
|
}
|
|
|
|
sourceMap := make(map[string]*models.Script)
|
|
targetMap := make(map[string]*models.Script)
|
|
|
|
for _, s := range source {
|
|
sourceMap[scriptCompareKey(s)] = s
|
|
}
|
|
for _, s := range target {
|
|
targetMap[scriptCompareKey(s)] = s
|
|
}
|
|
|
|
for _, name := range sortedKeys(sourceMap) {
|
|
srcScript := sourceMap[name]
|
|
if tgtScript, exists := targetMap[name]; !exists {
|
|
diff.Missing = append(diff.Missing, srcScript)
|
|
} else if changes := compareScriptDetails(srcScript, tgtScript); len(changes) > 0 {
|
|
diff.Modified = append(diff.Modified, &ScriptChange{
|
|
Name: srcScript.Name,
|
|
Source: srcScript,
|
|
Target: tgtScript,
|
|
Changes: changes,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, name := range sortedKeys(targetMap) {
|
|
tgtScript := targetMap[name]
|
|
if _, exists := sourceMap[name]; !exists {
|
|
diff.Extra = append(diff.Extra, tgtScript)
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
func scriptCompareKey(script *models.Script) string {
|
|
return fmt.Sprintf("%d:%d:%s", script.Priority, script.Sequence, script.SQLName())
|
|
}
|
|
|
|
func compareScriptDetails(source, target *models.Script) map[string]any {
|
|
changes := make(map[string]any)
|
|
|
|
if source.SQL != target.SQL {
|
|
changes["sql"] = map[string]string{"source": source.SQL, "target": target.SQL}
|
|
}
|
|
if source.Rollback != target.Rollback {
|
|
changes["rollback"] = map[string]string{"source": source.Rollback, "target": target.Rollback}
|
|
}
|
|
if !reflect.DeepEqual(source.RunAfter, target.RunAfter) {
|
|
changes["run_after"] = map[string][]string{"source": source.RunAfter, "target": target.RunAfter}
|
|
}
|
|
if source.Schema != target.Schema {
|
|
changes["schema"] = map[string]string{"source": source.Schema, "target": target.Schema}
|
|
}
|
|
if source.Version != target.Version {
|
|
changes["version"] = map[string]string{"source": source.Version, "target": target.Version}
|
|
}
|
|
if source.Priority != target.Priority {
|
|
changes["priority"] = map[string]int{"source": source.Priority, "target": target.Priority}
|
|
}
|
|
if source.Sequence != target.Sequence {
|
|
changes["sequence"] = map[string]uint{"source": source.Sequence, "target": target.Sequence}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
// Helper function to check if a diff is empty
|
|
func isEmpty(v any) bool {
|
|
switch d := v.(type) {
|
|
case *TableDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *ColumnDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *IndexDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *ConstraintDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *RelationshipDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *ViewDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *SequenceDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
case *ScriptDiff:
|
|
return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ComputeSummary generates a summary with counts from a DiffResult
|
|
func ComputeSummary(result *DiffResult) *Summary {
|
|
summary := &Summary{}
|
|
|
|
if result.Schemas != nil {
|
|
summary.Schemas = SchemaSummary{
|
|
Missing: len(result.Schemas.Missing),
|
|
Extra: len(result.Schemas.Extra),
|
|
Modified: len(result.Schemas.Modified),
|
|
}
|
|
|
|
// Aggregate table/column/index/constraint counts
|
|
for _, schemaChange := range result.Schemas.Modified {
|
|
if schemaChange.Tables != nil {
|
|
summary.Tables.Missing += len(schemaChange.Tables.Missing)
|
|
summary.Tables.Extra += len(schemaChange.Tables.Extra)
|
|
summary.Tables.Modified += len(schemaChange.Tables.Modified)
|
|
|
|
for _, tableChange := range schemaChange.Tables.Modified {
|
|
if tableChange.Columns != nil {
|
|
summary.Columns.Missing += len(tableChange.Columns.Missing)
|
|
summary.Columns.Extra += len(tableChange.Columns.Extra)
|
|
summary.Columns.Modified += len(tableChange.Columns.Modified)
|
|
}
|
|
if tableChange.Indexes != nil {
|
|
summary.Indexes.Missing += len(tableChange.Indexes.Missing)
|
|
summary.Indexes.Extra += len(tableChange.Indexes.Extra)
|
|
summary.Indexes.Modified += len(tableChange.Indexes.Modified)
|
|
}
|
|
if tableChange.Constraints != nil {
|
|
summary.Constraints.Missing += len(tableChange.Constraints.Missing)
|
|
summary.Constraints.Extra += len(tableChange.Constraints.Extra)
|
|
summary.Constraints.Modified += len(tableChange.Constraints.Modified)
|
|
}
|
|
if tableChange.Relationships != nil {
|
|
summary.Relationships.Missing += len(tableChange.Relationships.Missing)
|
|
summary.Relationships.Extra += len(tableChange.Relationships.Extra)
|
|
summary.Relationships.Modified += len(tableChange.Relationships.Modified)
|
|
}
|
|
}
|
|
}
|
|
if schemaChange.Views != nil {
|
|
summary.Views.Missing += len(schemaChange.Views.Missing)
|
|
summary.Views.Extra += len(schemaChange.Views.Extra)
|
|
summary.Views.Modified += len(schemaChange.Views.Modified)
|
|
}
|
|
if schemaChange.Sequences != nil {
|
|
summary.Sequences.Missing += len(schemaChange.Sequences.Missing)
|
|
summary.Sequences.Extra += len(schemaChange.Sequences.Extra)
|
|
summary.Sequences.Modified += len(schemaChange.Sequences.Modified)
|
|
}
|
|
if schemaChange.Scripts != nil {
|
|
summary.Scripts.Missing += len(schemaChange.Scripts.Missing)
|
|
summary.Scripts.Extra += len(schemaChange.Scripts.Extra)
|
|
summary.Scripts.Modified += len(schemaChange.Scripts.Modified)
|
|
}
|
|
}
|
|
}
|
|
|
|
return summary
|
|
}
|