From f2d500f98da9c9d8f86cad3c22cf10147a9d4b8c Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 31 Jan 2026 21:30:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(merge):=20=F0=9F=8E=89=20Add=20support=20f?= =?UTF-8?q?or=20constraints=20and=20indexes=20in=20merge=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance MergeResult to track added constraints and indexes. * Update merge logic to increment counters for added constraints and indexes. * Modify GetMergeSummary to include constraints and indexes in the output. * Add comprehensive tests for merging constraints and indexes. --- pkg/merge/merge.go | 23 +- pkg/merge/merge_test.go | 617 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 632 insertions(+), 8 deletions(-) create mode 100644 pkg/merge/merge_test.go diff --git a/pkg/merge/merge.go b/pkg/merge/merge.go index c6c64a2..63e7b08 100644 --- a/pkg/merge/merge.go +++ b/pkg/merge/merge.go @@ -12,14 +12,16 @@ import ( // MergeResult represents the result of a merge operation type MergeResult struct { - SchemasAdded int - TablesAdded int - ColumnsAdded int - RelationsAdded int - DomainsAdded int - EnumsAdded int - ViewsAdded int - SequencesAdded int + SchemasAdded int + TablesAdded int + ColumnsAdded int + ConstraintsAdded int + IndexesAdded int + RelationsAdded int + DomainsAdded int + EnumsAdded int + ViewsAdded int + SequencesAdded int } // MergeOptions contains options for merge operations @@ -171,6 +173,7 @@ func (r *MergeResult) mergeConstraints(table *models.Table, srcTable *models.Tab // Constraint doesn't exist, add it newConst := cloneConstraint(srcConst) table.Constraints[constName] = newConst + r.ConstraintsAdded++ } } } @@ -193,6 +196,7 @@ func (r *MergeResult) mergeIndexes(table *models.Table, srcTable *models.Table) // Index doesn't exist, add it newIdx := cloneIndex(srcIdx) table.Indexes[idxName] = newIdx + r.IndexesAdded++ } } } @@ -598,6 +602,8 @@ func GetMergeSummary(result *MergeResult) string { fmt.Sprintf("Schemas added: %d", result.SchemasAdded), fmt.Sprintf("Tables added: %d", result.TablesAdded), fmt.Sprintf("Columns added: %d", result.ColumnsAdded), + fmt.Sprintf("Constraints added: %d", result.ConstraintsAdded), + fmt.Sprintf("Indexes added: %d", result.IndexesAdded), fmt.Sprintf("Views added: %d", result.ViewsAdded), fmt.Sprintf("Sequences added: %d", result.SequencesAdded), fmt.Sprintf("Enums added: %d", result.EnumsAdded), @@ -606,6 +612,7 @@ func GetMergeSummary(result *MergeResult) string { } totalAdded := result.SchemasAdded + result.TablesAdded + result.ColumnsAdded + + result.ConstraintsAdded + result.IndexesAdded + result.ViewsAdded + result.SequencesAdded + result.EnumsAdded + result.RelationsAdded + result.DomainsAdded diff --git a/pkg/merge/merge_test.go b/pkg/merge/merge_test.go new file mode 100644 index 0000000..f93abef --- /dev/null +++ b/pkg/merge/merge_test.go @@ -0,0 +1,617 @@ +package merge + +import ( + "testing" + + "git.warky.dev/wdevs/relspecgo/pkg/models" +) + +func TestMergeDatabases_NilInputs(t *testing.T) { + result := MergeDatabases(nil, nil, nil) + if result == nil { + t.Fatal("Expected non-nil result") + } + if result.SchemasAdded != 0 { + t.Errorf("Expected 0 schemas added, got %d", result.SchemasAdded) + } +} + +func TestMergeDatabases_NewSchema(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + {Name: "public"}, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + {Name: "auth"}, + }, + } + + result := MergeDatabases(target, source, nil) + if result.SchemasAdded != 1 { + t.Errorf("Expected 1 schema added, got %d", result.SchemasAdded) + } + if len(target.Schemas) != 2 { + t.Errorf("Expected 2 schemas in target, got %d", len(target.Schemas)) + } +} + +func TestMergeDatabases_ExistingSchema(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + {Name: "public"}, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + {Name: "public"}, + }, + } + + result := MergeDatabases(target, source, nil) + if result.SchemasAdded != 0 { + t.Errorf("Expected 0 schemas added, got %d", result.SchemasAdded) + } + if len(target.Schemas) != 1 { + t.Errorf("Expected 1 schema in target, got %d", len(target.Schemas)) + } +} + +func TestMergeTables_NewTable(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "posts", + Schema: "public", + Columns: map[string]*models.Column{}, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.TablesAdded != 1 { + t.Errorf("Expected 1 table added, got %d", result.TablesAdded) + } + if len(target.Schemas[0].Tables) != 2 { + t.Errorf("Expected 2 tables in target schema, got %d", len(target.Schemas[0].Tables)) + } +} + +func TestMergeColumns_NewColumn(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{ + "id": {Name: "id", Type: "int"}, + }, + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{ + "email": {Name: "email", Type: "varchar"}, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.ColumnsAdded != 1 { + t.Errorf("Expected 1 column added, got %d", result.ColumnsAdded) + } + if len(target.Schemas[0].Tables[0].Columns) != 2 { + t.Errorf("Expected 2 columns in target table, got %d", len(target.Schemas[0].Tables[0].Columns)) + } +} + +func TestMergeConstraints_NewConstraint(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Constraints: map[string]*models.Constraint{}, + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Constraints: map[string]*models.Constraint{ + "ukey_users_email": { + Type: models.UniqueConstraint, + Columns: []string{"email"}, + Name: "ukey_users_email", + }, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.ConstraintsAdded != 1 { + t.Errorf("Expected 1 constraint added, got %d", result.ConstraintsAdded) + } + if len(target.Schemas[0].Tables[0].Constraints) != 1 { + t.Errorf("Expected 1 constraint in target table, got %d", len(target.Schemas[0].Tables[0].Constraints)) + } +} + +func TestMergeConstraints_NilConstraintsMap(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Constraints: nil, // Nil map + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Constraints: map[string]*models.Constraint{ + "ukey_users_email": { + Type: models.UniqueConstraint, + Columns: []string{"email"}, + Name: "ukey_users_email", + }, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.ConstraintsAdded != 1 { + t.Errorf("Expected 1 constraint added, got %d", result.ConstraintsAdded) + } + if target.Schemas[0].Tables[0].Constraints == nil { + t.Error("Expected constraints map to be initialized") + } + if len(target.Schemas[0].Tables[0].Constraints) != 1 { + t.Errorf("Expected 1 constraint in target table, got %d", len(target.Schemas[0].Tables[0].Constraints)) + } +} + +func TestMergeIndexes_NewIndex(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Indexes: map[string]*models.Index{}, + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Indexes: map[string]*models.Index{ + "idx_users_email": { + Name: "idx_users_email", + Columns: []string{"email"}, + }, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.IndexesAdded != 1 { + t.Errorf("Expected 1 index added, got %d", result.IndexesAdded) + } + if len(target.Schemas[0].Tables[0].Indexes) != 1 { + t.Errorf("Expected 1 index in target table, got %d", len(target.Schemas[0].Tables[0].Indexes)) + } +} + +func TestMergeIndexes_NilIndexesMap(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Indexes: nil, // Nil map + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + Indexes: map[string]*models.Index{ + "idx_users_email": { + Name: "idx_users_email", + Columns: []string{"email"}, + }, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.IndexesAdded != 1 { + t.Errorf("Expected 1 index added, got %d", result.IndexesAdded) + } + if target.Schemas[0].Tables[0].Indexes == nil { + t.Error("Expected indexes map to be initialized") + } + if len(target.Schemas[0].Tables[0].Indexes) != 1 { + t.Errorf("Expected 1 index in target table, got %d", len(target.Schemas[0].Tables[0].Indexes)) + } +} + +func TestMergeOptions_SkipTableNames(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{}, + }, + }, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "migrations", + Schema: "public", + Columns: map[string]*models.Column{}, + }, + }, + }, + }, + } + + opts := &MergeOptions{ + SkipTableNames: map[string]bool{ + "migrations": true, + }, + } + + result := MergeDatabases(target, source, opts) + if result.TablesAdded != 0 { + t.Errorf("Expected 0 tables added (skipped), got %d", result.TablesAdded) + } + if len(target.Schemas[0].Tables) != 1 { + t.Errorf("Expected 1 table in target schema, got %d", len(target.Schemas[0].Tables)) + } +} + +func TestMergeViews_NewView(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Views: []*models.View{}, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Views: []*models.View{ + { + Name: "user_summary", + Schema: "public", + Definition: "SELECT * FROM users", + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.ViewsAdded != 1 { + t.Errorf("Expected 1 view added, got %d", result.ViewsAdded) + } + if len(target.Schemas[0].Views) != 1 { + t.Errorf("Expected 1 view in target schema, got %d", len(target.Schemas[0].Views)) + } +} + +func TestMergeEnums_NewEnum(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Enums: []*models.Enum{}, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Enums: []*models.Enum{ + { + Name: "user_role", + Schema: "public", + Values: []string{"admin", "user"}, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.EnumsAdded != 1 { + t.Errorf("Expected 1 enum added, got %d", result.EnumsAdded) + } + if len(target.Schemas[0].Enums) != 1 { + t.Errorf("Expected 1 enum in target schema, got %d", len(target.Schemas[0].Enums)) + } +} + +func TestMergeDomains_NewDomain(t *testing.T) { + target := &models.Database{ + Domains: []*models.Domain{}, + } + source := &models.Database{ + Domains: []*models.Domain{ + { + Name: "auth", + Description: "Authentication domain", + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.DomainsAdded != 1 { + t.Errorf("Expected 1 domain added, got %d", result.DomainsAdded) + } + if len(target.Domains) != 1 { + t.Errorf("Expected 1 domain in target, got %d", len(target.Domains)) + } +} + +func TestMergeRelations_NewRelation(t *testing.T) { + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Relations: []*models.Relationship{}, + }, + }, + } + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Relations: []*models.Relationship{ + { + Name: "fk_posts_user", + Type: models.OneToMany, + FromTable: "posts", + FromColumns: []string{"user_id"}, + ToTable: "users", + ToColumns: []string{"id"}, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + if result.RelationsAdded != 1 { + t.Errorf("Expected 1 relation added, got %d", result.RelationsAdded) + } + if len(target.Schemas[0].Relations) != 1 { + t.Errorf("Expected 1 relation in target schema, got %d", len(target.Schemas[0].Relations)) + } +} + +func TestGetMergeSummary(t *testing.T) { + result := &MergeResult{ + SchemasAdded: 1, + TablesAdded: 2, + ColumnsAdded: 5, + ConstraintsAdded: 3, + IndexesAdded: 2, + ViewsAdded: 1, + } + + summary := GetMergeSummary(result) + if summary == "" { + t.Error("Expected non-empty summary") + } + if len(summary) < 50 { + t.Errorf("Summary seems too short: %s", summary) + } +} + +func TestGetMergeSummary_Nil(t *testing.T) { + summary := GetMergeSummary(nil) + if summary == "" { + t.Error("Expected non-empty summary for nil result") + } +} + +func TestComplexMerge(t *testing.T) { + // Target with existing structure + target := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{ + "id": {Name: "id", Type: "int"}, + }, + Constraints: map[string]*models.Constraint{}, + Indexes: map[string]*models.Index{}, + }, + }, + }, + }, + } + + // Source with new columns, constraints, and indexes + source := &models.Database{ + Schemas: []*models.Schema{ + { + Name: "public", + Tables: []*models.Table{ + { + Name: "users", + Schema: "public", + Columns: map[string]*models.Column{ + "email": {Name: "email", Type: "varchar"}, + "guid": {Name: "guid", Type: "uuid"}, + }, + Constraints: map[string]*models.Constraint{ + "ukey_users_email": { + Type: models.UniqueConstraint, + Columns: []string{"email"}, + Name: "ukey_users_email", + }, + "ukey_users_guid": { + Type: models.UniqueConstraint, + Columns: []string{"guid"}, + Name: "ukey_users_guid", + }, + }, + Indexes: map[string]*models.Index{ + "idx_users_email": { + Name: "idx_users_email", + Columns: []string{"email"}, + }, + }, + }, + }, + }, + }, + } + + result := MergeDatabases(target, source, nil) + + // Verify counts + if result.ColumnsAdded != 2 { + t.Errorf("Expected 2 columns added, got %d", result.ColumnsAdded) + } + if result.ConstraintsAdded != 2 { + t.Errorf("Expected 2 constraints added, got %d", result.ConstraintsAdded) + } + if result.IndexesAdded != 1 { + t.Errorf("Expected 1 index added, got %d", result.IndexesAdded) + } + + // Verify target has merged data + table := target.Schemas[0].Tables[0] + if len(table.Columns) != 3 { + t.Errorf("Expected 3 columns in merged table, got %d", len(table.Columns)) + } + if len(table.Constraints) != 2 { + t.Errorf("Expected 2 constraints in merged table, got %d", len(table.Constraints)) + } + if len(table.Indexes) != 1 { + t.Errorf("Expected 1 index in merged table, got %d", len(table.Indexes)) + } + + // Verify specific constraint + if _, exists := table.Constraints["ukey_users_guid"]; !exists { + t.Error("Expected ukey_users_guid constraint to exist") + } +}