- Introduced equivalent base types and variants for PostgreSQL types to normalize type comparisons. - Added functions for normalizing SQL types and retrieving equivalent type variants. - Updated migration writer to handle type alterations with checks for existing types. - Implemented logic to create necessary extensions (e.g., pg_trgm) based on schema requirements. - Enhanced tests to cover new functionality for type normalization and migration handling. - Improved handling of GIN indexes to use appropriate operator classes based on column types.
680 lines
16 KiB
Go
680 lines
16 KiB
Go
package merge
|
|
|
|
import (
|
|
"strings"
|
|
"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 TestMergeColumns_TypeConflictIsDetected(t *testing.T) {
|
|
target := &models.Database{
|
|
Schemas: []*models.Schema{
|
|
{
|
|
Name: "public",
|
|
Tables: []*models.Table{
|
|
{
|
|
Name: "users",
|
|
Schema: "public",
|
|
Columns: map[string]*models.Column{
|
|
"email": {Name: "email", Type: "varchar", Length: 255},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
source := &models.Database{
|
|
Schemas: []*models.Schema{
|
|
{
|
|
Name: "public",
|
|
Tables: []*models.Table{
|
|
{
|
|
Name: "users",
|
|
Schema: "public",
|
|
Columns: map[string]*models.Column{
|
|
"email": {Name: "email", Type: "text"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := MergeDatabases(target, source, nil)
|
|
|
|
if len(result.TypeConflicts) != 1 {
|
|
t.Fatalf("Expected 1 type conflict, got %d", len(result.TypeConflicts))
|
|
}
|
|
conflict := result.TypeConflicts[0]
|
|
if conflict.Schema != "public" || conflict.Table != "users" || conflict.Column != "email" {
|
|
t.Fatalf("Unexpected conflict location: %+v", conflict)
|
|
}
|
|
if conflict.TargetType != "varchar(255)" {
|
|
t.Fatalf("Expected target type varchar(255), got %q", conflict.TargetType)
|
|
}
|
|
if conflict.SourceType != "text" {
|
|
t.Fatalf("Expected source type text, got %q", conflict.SourceType)
|
|
}
|
|
|
|
if got := target.Schemas[0].Tables[0].Columns["email"].Type; got != "varchar" {
|
|
t.Fatalf("Expected target column type to remain unchanged, got %q", got)
|
|
}
|
|
}
|
|
|
|
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,
|
|
TypeConflicts: []ColumnTypeConflict{
|
|
{Schema: "public", Table: "users", Column: "email", TargetType: "varchar(255)", SourceType: "text"},
|
|
},
|
|
}
|
|
|
|
summary := GetMergeSummary(result)
|
|
if summary == "" {
|
|
t.Error("Expected non-empty summary")
|
|
}
|
|
if len(summary) < 50 {
|
|
t.Errorf("Summary seems too short: %s", summary)
|
|
}
|
|
if !strings.Contains(summary, "Type conflicts: 1") {
|
|
t.Errorf("Expected type conflict count in summary, got: %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")
|
|
}
|
|
}
|