package diff import ( "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" ) func TestCompareDatabases(t *testing.T) { tests := []struct { name string source *models.Database target *models.Database want func(*DiffResult) bool }{ { name: "identical databases", source: &models.Database{ Name: "source", Schemas: []*models.Schema{}, }, target: &models.Database{ Name: "target", Schemas: []*models.Schema{}, }, want: func(r *DiffResult) bool { return r.Source == "source" && r.Target == "target" && len(r.Schemas.Missing) == 0 && len(r.Schemas.Extra) == 0 }, }, { name: "different schemas", source: &models.Database{ Name: "source", Schemas: []*models.Schema{ {Name: "public"}, }, }, target: &models.Database{ Name: "target", Schemas: []*models.Schema{}, }, want: func(r *DiffResult) bool { return len(r.Schemas.Missing) == 1 && r.Schemas.Missing[0].Name == "public" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := CompareDatabases(tt.source, tt.target) if !tt.want(got) { t.Errorf("CompareDatabases() result doesn't match expectations") } }) } } func TestCompareColumns(t *testing.T) { tests := []struct { name string source map[string]*models.Column target map[string]*models.Column want func(*ColumnDiff) bool }{ { name: "identical columns", source: map[string]*models.Column{}, target: map[string]*models.Column{}, want: func(d *ColumnDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing column", source: map[string]*models.Column{ "id": {Name: "id", Type: "integer"}, }, target: map[string]*models.Column{}, want: func(d *ColumnDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "id" }, }, { name: "extra column", source: map[string]*models.Column{}, target: map[string]*models.Column{ "id": {Name: "id", Type: "integer"}, }, want: func(d *ColumnDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "id" }, }, { name: "modified column type", source: map[string]*models.Column{ "id": {Name: "id", Type: "integer"}, }, target: map[string]*models.Column{ "id": {Name: "id", Type: "bigint"}, }, want: func(d *ColumnDiff) bool { return len(d.Modified) == 1 && d.Modified[0].Name == "id" && d.Modified[0].Changes["type"] != nil }, }, { name: "modified column nullable", source: map[string]*models.Column{ "name": {Name: "name", Type: "text", NotNull: true}, }, target: map[string]*models.Column{ "name": {Name: "name", Type: "text", NotNull: false}, }, want: func(d *ColumnDiff) bool { return len(d.Modified) == 1 && d.Modified[0].Changes["not_null"] != nil }, }, { name: "modified column length", source: map[string]*models.Column{ "name": {Name: "name", Type: "varchar", Length: 100}, }, target: map[string]*models.Column{ "name": {Name: "name", Type: "varchar", Length: 255}, }, want: func(d *ColumnDiff) bool { return len(d.Modified) == 1 && d.Modified[0].Changes["length"] != nil }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareColumns(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareColumns() result doesn't match expectations") } }) } } func TestCompareColumnDetails(t *testing.T) { tests := []struct { name string source *models.Column target *models.Column want int // number of changes }{ { name: "identical columns", source: &models.Column{Name: "id", Type: "integer"}, target: &models.Column{Name: "id", Type: "integer"}, want: 0, }, { name: "type change", source: &models.Column{Name: "id", Type: "integer"}, target: &models.Column{Name: "id", Type: "bigint"}, want: 1, }, { name: "length change", source: &models.Column{Name: "name", Type: "varchar", Length: 100}, target: &models.Column{Name: "name", Type: "varchar", Length: 255}, want: 1, }, { name: "precision change", source: &models.Column{Name: "price", Type: "numeric", Precision: 10}, target: &models.Column{Name: "price", Type: "numeric", Precision: 12}, want: 1, }, { name: "scale change", source: &models.Column{Name: "price", Type: "numeric", Scale: 2}, target: &models.Column{Name: "price", Type: "numeric", Scale: 4}, want: 1, }, { name: "not null change", source: &models.Column{Name: "name", Type: "text", NotNull: true}, target: &models.Column{Name: "name", Type: "text", NotNull: false}, want: 1, }, { name: "auto increment change", source: &models.Column{Name: "id", Type: "integer", AutoIncrement: true}, target: &models.Column{Name: "id", Type: "integer", AutoIncrement: false}, want: 1, }, { name: "primary key change", source: &models.Column{Name: "id", Type: "integer", IsPrimaryKey: true}, target: &models.Column{Name: "id", Type: "integer", IsPrimaryKey: false}, want: 1, }, { name: "multiple changes", source: &models.Column{Name: "id", Type: "integer", NotNull: true, AutoIncrement: true}, target: &models.Column{Name: "id", Type: "bigint", NotNull: false, AutoIncrement: false}, want: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareColumnDetails(tt.source, tt.target) if len(got) != tt.want { t.Errorf("compareColumnDetails() = %d changes, want %d", len(got), tt.want) } }) } } func TestCompareIndexes(t *testing.T) { tests := []struct { name string source map[string]*models.Index target map[string]*models.Index want func(*IndexDiff) bool }{ { name: "identical indexes", source: map[string]*models.Index{}, target: map[string]*models.Index{}, want: func(d *IndexDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing index", source: map[string]*models.Index{ "idx_name": {Name: "idx_name", Columns: []string{"name"}}, }, target: map[string]*models.Index{}, want: func(d *IndexDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "idx_name" }, }, { name: "extra index", source: map[string]*models.Index{}, target: map[string]*models.Index{ "idx_name": {Name: "idx_name", Columns: []string{"name"}}, }, want: func(d *IndexDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "idx_name" }, }, { name: "modified index uniqueness", source: map[string]*models.Index{ "idx_name": {Name: "idx_name", Columns: []string{"name"}, Unique: false}, }, target: map[string]*models.Index{ "idx_name": {Name: "idx_name", Columns: []string{"name"}, Unique: true}, }, want: func(d *IndexDiff) bool { return len(d.Modified) == 1 && d.Modified[0].Name == "idx_name" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareIndexes(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareIndexes() result doesn't match expectations") } }) } } func TestCompareConstraints(t *testing.T) { tests := []struct { name string source map[string]*models.Constraint target map[string]*models.Constraint want func(*ConstraintDiff) bool }{ { name: "identical constraints", source: map[string]*models.Constraint{}, target: map[string]*models.Constraint{}, want: func(d *ConstraintDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing constraint", source: map[string]*models.Constraint{ "pk_id": {Name: "pk_id", Type: "PRIMARY KEY", Columns: []string{"id"}}, }, target: map[string]*models.Constraint{}, want: func(d *ConstraintDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "pk_id" }, }, { name: "extra constraint", source: map[string]*models.Constraint{}, target: map[string]*models.Constraint{ "pk_id": {Name: "pk_id", Type: "PRIMARY KEY", Columns: []string{"id"}}, }, want: func(d *ConstraintDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "pk_id" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareConstraints(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareConstraints() result doesn't match expectations") } }) } } func TestCompareRelationships(t *testing.T) { tests := []struct { name string source map[string]*models.Relationship target map[string]*models.Relationship want func(*RelationshipDiff) bool }{ { name: "identical relationships", source: map[string]*models.Relationship{}, target: map[string]*models.Relationship{}, want: func(d *RelationshipDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing relationship", source: map[string]*models.Relationship{ "fk_user": {Name: "fk_user", Type: "FOREIGN KEY"}, }, target: map[string]*models.Relationship{}, want: func(d *RelationshipDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "fk_user" }, }, { name: "extra relationship", source: map[string]*models.Relationship{}, target: map[string]*models.Relationship{ "fk_user": {Name: "fk_user", Type: "FOREIGN KEY"}, }, want: func(d *RelationshipDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "fk_user" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareRelationships(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareRelationships() result doesn't match expectations") } }) } } func TestCompareTables(t *testing.T) { tests := []struct { name string source []*models.Table target []*models.Table want func(*TableDiff) bool }{ { name: "identical tables", source: []*models.Table{}, target: []*models.Table{}, want: func(d *TableDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing table", source: []*models.Table{ {Name: "users", Schema: "public"}, }, target: []*models.Table{}, want: func(d *TableDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "users" }, }, { name: "extra table", source: []*models.Table{}, target: []*models.Table{ {Name: "users", Schema: "public"}, }, want: func(d *TableDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "users" }, }, { name: "modified table", source: []*models.Table{ { Name: "users", Schema: "public", Columns: map[string]*models.Column{ "id": {Name: "id", Type: "integer"}, }, }, }, target: []*models.Table{ { Name: "users", Schema: "public", Columns: map[string]*models.Column{ "id": {Name: "id", Type: "bigint"}, }, }, }, want: func(d *TableDiff) bool { return len(d.Modified) == 1 && d.Modified[0].Name == "users" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareTables(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareTables() result doesn't match expectations") } }) } } func TestCompareSchemas(t *testing.T) { tests := []struct { name string source []*models.Schema target []*models.Schema want func(*SchemaDiff) bool }{ { name: "identical schemas", source: []*models.Schema{}, target: []*models.Schema{}, want: func(d *SchemaDiff) bool { return len(d.Missing) == 0 && len(d.Extra) == 0 && len(d.Modified) == 0 }, }, { name: "missing schema", source: []*models.Schema{ {Name: "public"}, }, target: []*models.Schema{}, want: func(d *SchemaDiff) bool { return len(d.Missing) == 1 && d.Missing[0].Name == "public" }, }, { name: "extra schema", source: []*models.Schema{}, target: []*models.Schema{ {Name: "public"}, }, want: func(d *SchemaDiff) bool { return len(d.Extra) == 1 && d.Extra[0].Name == "public" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := compareSchemas(tt.source, tt.target) if !tt.want(got) { t.Errorf("compareSchemas() result doesn't match expectations") } }) } } func TestIsEmpty(t *testing.T) { tests := []struct { name string v interface{} want bool }{ {"empty ColumnDiff", &ColumnDiff{Missing: []*models.Column{}, Extra: []*models.Column{}, Modified: []*ColumnChange{}}, true}, {"ColumnDiff with missing", &ColumnDiff{Missing: []*models.Column{{Name: "id"}}, Extra: []*models.Column{}, Modified: []*ColumnChange{}}, false}, {"ColumnDiff with extra", &ColumnDiff{Missing: []*models.Column{}, Extra: []*models.Column{{Name: "id"}}, Modified: []*ColumnChange{}}, false}, {"empty IndexDiff", &IndexDiff{Missing: []*models.Index{}, Extra: []*models.Index{}, Modified: []*IndexChange{}}, true}, {"IndexDiff with missing", &IndexDiff{Missing: []*models.Index{{Name: "idx"}}, Extra: []*models.Index{}, Modified: []*IndexChange{}}, false}, {"empty TableDiff", &TableDiff{Missing: []*models.Table{}, Extra: []*models.Table{}, Modified: []*TableChange{}}, true}, {"TableDiff with extra", &TableDiff{Missing: []*models.Table{}, Extra: []*models.Table{{Name: "users"}}, Modified: []*TableChange{}}, false}, {"empty ConstraintDiff", &ConstraintDiff{Missing: []*models.Constraint{}, Extra: []*models.Constraint{}, Modified: []*ConstraintChange{}}, true}, {"empty RelationshipDiff", &RelationshipDiff{Missing: []*models.Relationship{}, Extra: []*models.Relationship{}, Modified: []*RelationshipChange{}}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isEmpty(tt.v) if got != tt.want { t.Errorf("isEmpty() = %v, want %v", got, tt.want) } }) } } func TestComputeSummary(t *testing.T) { tests := []struct { name string result *DiffResult want func(*Summary) bool }{ { name: "empty diff", result: &DiffResult{ Schemas: &SchemaDiff{ Missing: []*models.Schema{}, Extra: []*models.Schema{}, Modified: []*SchemaChange{}, }, }, want: func(s *Summary) bool { return s.Schemas.Missing == 0 && s.Schemas.Extra == 0 && s.Schemas.Modified == 0 }, }, { name: "schemas with differences", result: &DiffResult{ Schemas: &SchemaDiff{ Missing: []*models.Schema{{Name: "schema1"}}, Extra: []*models.Schema{{Name: "schema2"}, {Name: "schema3"}}, Modified: []*SchemaChange{ {Name: "public"}, }, }, }, want: func(s *Summary) bool { return s.Schemas.Missing == 1 && s.Schemas.Extra == 2 && s.Schemas.Modified == 1 }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ComputeSummary(tt.result) if !tt.want(got) { t.Errorf("ComputeSummary() result doesn't match expectations") } }) } }