package bun import ( "os" "path/filepath" "strings" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/writers" ) func TestWriter_WriteTable(t *testing.T) { // Create a simple table table := models.InitTable("users", "public") table.Columns["id"] = &models.Column{ Name: "id", Type: "bigint", NotNull: true, IsPrimaryKey: true, AutoIncrement: true, Sequence: 1, } table.Columns["email"] = &models.Column{ Name: "email", Type: "varchar", Length: 255, NotNull: false, Sequence: 2, } table.Columns["created_at"] = &models.Column{ Name: "created_at", Type: "timestamp", NotNull: true, Sequence: 3, } // Create writer opts := &writers.WriterOptions{ PackageName: "models", Metadata: map[string]interface{}{ "generate_table_name": true, "generate_get_id": true, }, } writer := NewWriter(opts) // Write to temporary file tmpDir := t.TempDir() opts.OutputPath = filepath.Join(tmpDir, "test.go") err := writer.WriteTable(table) if err != nil { t.Fatalf("WriteTable failed: %v", err) } // Read the generated file content, err := os.ReadFile(opts.OutputPath) if err != nil { t.Fatalf("Failed to read generated file: %v", err) } generated := string(content) // Verify key elements are present expectations := []string{ "package models", "type ModelPublicUser struct", "bun.BaseModel", "table:public.users", "alias:users", "ID", "int64", "Email", "resolvespec_common.SqlString", "CreatedAt", "resolvespec_common.SqlTime", "bun:\"id", "bun:\"email", "func (m ModelPublicUser) TableName() string", "return \"public.users\"", "func (m ModelPublicUser) GetID() int64", } for _, expected := range expectations { if !strings.Contains(generated, expected) { t.Errorf("Generated code missing expected content: %q\nGenerated:\n%s", expected, generated) } } // Verify Bun-specific elements if !strings.Contains(generated, "bun:\"id,type:bigint,pk,") { t.Errorf("Missing Bun-style primary key tag") } } func TestWriter_WriteDatabase_MultiFile(t *testing.T) { // Create a database with two tables db := models.InitDatabase("testdb") schema := models.InitSchema("public") // Table 1: users users := models.InitTable("users", "public") users.Columns["id"] = &models.Column{ Name: "id", Type: "bigint", NotNull: true, IsPrimaryKey: true, } schema.Tables = append(schema.Tables, users) // Table 2: posts posts := models.InitTable("posts", "public") posts.Columns["id"] = &models.Column{ Name: "id", Type: "bigint", NotNull: true, IsPrimaryKey: true, } posts.Columns["user_id"] = &models.Column{ Name: "user_id", Type: "bigint", NotNull: true, } posts.Constraints["fk_user"] = &models.Constraint{ Name: "fk_user", Type: models.ForeignKeyConstraint, Columns: []string{"user_id"}, ReferencedTable: "users", ReferencedSchema: "public", ReferencedColumns: []string{"id"}, OnDelete: "CASCADE", } schema.Tables = append(schema.Tables, posts) db.Schemas = append(db.Schemas, schema) // Create writer with multi-file mode tmpDir := t.TempDir() opts := &writers.WriterOptions{ PackageName: "models", OutputPath: tmpDir, Metadata: map[string]interface{}{ "multi_file": true, }, } writer := NewWriter(opts) err := writer.WriteDatabase(db) if err != nil { t.Fatalf("WriteDatabase failed: %v", err) } // Verify two files were created expectedFiles := []string{ "sql_public_users.go", "sql_public_posts.go", } for _, filename := range expectedFiles { filepath := filepath.Join(tmpDir, filename) if _, err := os.Stat(filepath); os.IsNotExist(err) { t.Errorf("Expected file not created: %s", filename) } } // Check posts file contains relationship postsContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_public_posts.go")) if err != nil { t.Fatalf("Failed to read posts file: %v", err) } postsStr := string(postsContent) // Verify relationship is present with Bun format // Should now be RelUserID (has-one) instead of USE if !strings.Contains(postsStr, "RelUserID") { t.Errorf("Missing relationship field RelUserID (new naming convention)") } if !strings.Contains(postsStr, "rel:has-one") { t.Errorf("Missing Bun relationship tag: %s", postsStr) } // Check users file contains has-many relationship usersContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_public_users.go")) if err != nil { t.Fatalf("Failed to read users file: %v", err) } usersStr := string(usersContent) // Should have RelUserIDPublicPosts (has-many) field - includes schema prefix if !strings.Contains(usersStr, "RelUserIDPublicPosts") { t.Errorf("Missing has-many relationship field RelUserIDPublicPosts") } } func TestWriter_MultipleReferencesToSameTable(t *testing.T) { // Test scenario: api_event table with multiple foreign keys to filepointer table db := models.InitDatabase("testdb") schema := models.InitSchema("org") // Filepointer table filepointer := models.InitTable("filepointer", "org") filepointer.Columns["id_filepointer"] = &models.Column{ Name: "id_filepointer", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } schema.Tables = append(schema.Tables, filepointer) // API event table with two foreign keys to filepointer apiEvent := models.InitTable("api_event", "org") apiEvent.Columns["id_api_event"] = &models.Column{ Name: "id_api_event", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } apiEvent.Columns["rid_filepointer_request"] = &models.Column{ Name: "rid_filepointer_request", Type: "bigint", NotNull: false, } apiEvent.Columns["rid_filepointer_response"] = &models.Column{ Name: "rid_filepointer_response", Type: "bigint", NotNull: false, } // Add constraints apiEvent.Constraints["fk_request"] = &models.Constraint{ Name: "fk_request", Type: models.ForeignKeyConstraint, Columns: []string{"rid_filepointer_request"}, ReferencedTable: "filepointer", ReferencedSchema: "org", ReferencedColumns: []string{"id_filepointer"}, } apiEvent.Constraints["fk_response"] = &models.Constraint{ Name: "fk_response", Type: models.ForeignKeyConstraint, Columns: []string{"rid_filepointer_response"}, ReferencedTable: "filepointer", ReferencedSchema: "org", ReferencedColumns: []string{"id_filepointer"}, } schema.Tables = append(schema.Tables, apiEvent) db.Schemas = append(db.Schemas, schema) // Create writer tmpDir := t.TempDir() opts := &writers.WriterOptions{ PackageName: "models", OutputPath: tmpDir, Metadata: map[string]interface{}{ "multi_file": true, }, } writer := NewWriter(opts) err := writer.WriteDatabase(db) if err != nil { t.Fatalf("WriteDatabase failed: %v", err) } // Read the api_event file apiEventContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_api_event.go")) if err != nil { t.Fatalf("Failed to read api_event file: %v", err) } contentStr := string(apiEventContent) // Verify both relationships have unique names based on column names expectations := []struct { fieldName string tag string }{ {"RelRIDFilepointerRequest", "join:rid_filepointer_request=id_filepointer"}, {"RelRIDFilepointerResponse", "join:rid_filepointer_response=id_filepointer"}, } for _, exp := range expectations { if !strings.Contains(contentStr, exp.fieldName) { t.Errorf("Missing relationship field: %s\nGenerated:\n%s", exp.fieldName, contentStr) } if !strings.Contains(contentStr, exp.tag) { t.Errorf("Missing relationship tag: %s\nGenerated:\n%s", exp.tag, contentStr) } } // Verify NO duplicate field names (old behavior would create duplicate "FIL" fields) if strings.Contains(contentStr, "FIL *ModelFilepointer") { t.Errorf("Found old prefix-based naming (FIL), should use column-based naming") } // Also verify has-many relationships on filepointer table filepointerContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_filepointer.go")) if err != nil { t.Fatalf("Failed to read filepointer file: %v", err) } filepointerStr := string(filepointerContent) // Should have two different has-many relationships with unique names hasManyExpectations := []string{ "RelRIDFilepointerRequestOrgAPIEvents", // Has many via rid_filepointer_request "RelRIDFilepointerResponseOrgAPIEvents", // Has many via rid_filepointer_response } for _, exp := range hasManyExpectations { if !strings.Contains(filepointerStr, exp) { t.Errorf("Missing has-many relationship field: %s\nGenerated:\n%s", exp, filepointerStr) } } } func TestWriter_MultipleHasManyRelationships(t *testing.T) { // Test scenario: api_provider table referenced by multiple tables via rid_api_provider db := models.InitDatabase("testdb") schema := models.InitSchema("org") // Owner table owner := models.InitTable("owner", "org") owner.Columns["id_owner"] = &models.Column{ Name: "id_owner", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } schema.Tables = append(schema.Tables, owner) // API Provider table apiProvider := models.InitTable("api_provider", "org") apiProvider.Columns["id_api_provider"] = &models.Column{ Name: "id_api_provider", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } apiProvider.Columns["rid_owner"] = &models.Column{ Name: "rid_owner", Type: "bigint", NotNull: true, } apiProvider.Constraints["fk_owner"] = &models.Constraint{ Name: "fk_owner", Type: models.ForeignKeyConstraint, Columns: []string{"rid_owner"}, ReferencedTable: "owner", ReferencedSchema: "org", ReferencedColumns: []string{"id_owner"}, } schema.Tables = append(schema.Tables, apiProvider) // Login table login := models.InitTable("login", "org") login.Columns["id_login"] = &models.Column{ Name: "id_login", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } login.Columns["rid_api_provider"] = &models.Column{ Name: "rid_api_provider", Type: "bigint", NotNull: true, } login.Constraints["fk_api_provider"] = &models.Constraint{ Name: "fk_api_provider", Type: models.ForeignKeyConstraint, Columns: []string{"rid_api_provider"}, ReferencedTable: "api_provider", ReferencedSchema: "org", ReferencedColumns: []string{"id_api_provider"}, } schema.Tables = append(schema.Tables, login) // Filepointer table filepointer := models.InitTable("filepointer", "org") filepointer.Columns["id_filepointer"] = &models.Column{ Name: "id_filepointer", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } filepointer.Columns["rid_api_provider"] = &models.Column{ Name: "rid_api_provider", Type: "bigint", NotNull: true, } filepointer.Constraints["fk_api_provider"] = &models.Constraint{ Name: "fk_api_provider", Type: models.ForeignKeyConstraint, Columns: []string{"rid_api_provider"}, ReferencedTable: "api_provider", ReferencedSchema: "org", ReferencedColumns: []string{"id_api_provider"}, } schema.Tables = append(schema.Tables, filepointer) // API Event table apiEvent := models.InitTable("api_event", "org") apiEvent.Columns["id_api_event"] = &models.Column{ Name: "id_api_event", Type: "bigserial", NotNull: true, IsPrimaryKey: true, } apiEvent.Columns["rid_api_provider"] = &models.Column{ Name: "rid_api_provider", Type: "bigint", NotNull: true, } apiEvent.Constraints["fk_api_provider"] = &models.Constraint{ Name: "fk_api_provider", Type: models.ForeignKeyConstraint, Columns: []string{"rid_api_provider"}, ReferencedTable: "api_provider", ReferencedSchema: "org", ReferencedColumns: []string{"id_api_provider"}, } schema.Tables = append(schema.Tables, apiEvent) db.Schemas = append(db.Schemas, schema) // Create writer tmpDir := t.TempDir() opts := &writers.WriterOptions{ PackageName: "models", OutputPath: tmpDir, Metadata: map[string]interface{}{ "multi_file": true, }, } writer := NewWriter(opts) err := writer.WriteDatabase(db) if err != nil { t.Fatalf("WriteDatabase failed: %v", err) } // Read the api_provider file apiProviderContent, err := os.ReadFile(filepath.Join(tmpDir, "sql_org_api_provider.go")) if err != nil { t.Fatalf("Failed to read api_provider file: %v", err) } contentStr := string(apiProviderContent) // Verify all has-many relationships have unique names hasManyExpectations := []string{ "RelRIDAPIProviderOrgLogins", // Has many via Login "RelRIDAPIProviderOrgFilepointers", // Has many via Filepointer "RelRIDAPIProviderOrgAPIEvents", // Has many via APIEvent "RelRIDOwner", // Has one via rid_owner } for _, exp := range hasManyExpectations { if !strings.Contains(contentStr, exp) { t.Errorf("Missing relationship field: %s\nGenerated:\n%s", exp, contentStr) } } // Verify NO duplicate field names // Count occurrences of "RelRIDAPIProvider" fields - should have 3 unique ones count := strings.Count(contentStr, "RelRIDAPIProvider") if count != 3 { t.Errorf("Expected 3 RelRIDAPIProvider* fields, found %d\nGenerated:\n%s", count, contentStr) } // Verify no duplicate declarations (would cause compilation error) duplicatePattern := "RelRIDAPIProviders []*Model" if strings.Contains(contentStr, duplicatePattern) { t.Errorf("Found duplicate field declaration pattern, fields should be unique") } } func TestWriter_FieldNameCollision(t *testing.T) { // Test scenario: table with columns that would conflict with generated method names table := models.InitTable("audit_table", "audit") table.Columns["id_audit_table"] = &models.Column{ Name: "id_audit_table", Type: "smallint", NotNull: true, IsPrimaryKey: true, Sequence: 1, } table.Columns["table_name"] = &models.Column{ Name: "table_name", Type: "varchar", Length: 100, NotNull: true, Sequence: 2, } table.Columns["table_schema"] = &models.Column{ Name: "table_schema", Type: "varchar", Length: 100, NotNull: true, Sequence: 3, } // Create writer tmpDir := t.TempDir() opts := &writers.WriterOptions{ PackageName: "models", OutputPath: filepath.Join(tmpDir, "test.go"), } writer := NewWriter(opts) err := writer.WriteTable(table) if err != nil { t.Fatalf("WriteTable failed: %v", err) } // Read the generated file content, err := os.ReadFile(opts.OutputPath) if err != nil { t.Fatalf("Failed to read generated file: %v", err) } generated := string(content) // Verify that TableName field was renamed to TableName_ to avoid collision if !strings.Contains(generated, "TableName_") { t.Errorf("Expected field 'TableName_' (with underscore) but not found\nGenerated:\n%s", generated) } // Verify the struct tag still references the correct database column if !strings.Contains(generated, `bun:"table_name,`) { t.Errorf("Expected bun tag to reference 'table_name' column\nGenerated:\n%s", generated) } // Verify the TableName() method still exists and doesn't conflict if !strings.Contains(generated, "func (m ModelAuditAuditTable) TableName() string") { t.Errorf("TableName() method should still be generated\nGenerated:\n%s", generated) } // Verify NO field named just "TableName" (without underscore) if strings.Contains(generated, "TableName resolvespec_common") || strings.Contains(generated, "TableName string") { t.Errorf("Field 'TableName' without underscore should not exist (would conflict with method)\nGenerated:\n%s", generated) } } func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) { mapper := NewTypeMapper() tests := []struct { sqlType string notNull bool want string }{ {"bigint", true, "int64"}, {"bigint", false, "resolvespec_common.SqlInt64"}, {"varchar", true, "resolvespec_common.SqlString"}, // Bun uses sql types even for NOT NULL strings {"varchar", false, "resolvespec_common.SqlString"}, {"timestamp", true, "resolvespec_common.SqlTime"}, {"timestamp", false, "resolvespec_common.SqlTime"}, {"date", false, "resolvespec_common.SqlDate"}, {"boolean", true, "bool"}, {"boolean", false, "resolvespec_common.SqlBool"}, {"uuid", false, "resolvespec_common.SqlUUID"}, {"jsonb", false, "resolvespec_common.SqlJSONB"}, } for _, tt := range tests { t.Run(tt.sqlType, func(t *testing.T) { result := mapper.SQLTypeToGoType(tt.sqlType, tt.notNull) if result != tt.want { t.Errorf("SQLTypeToGoType(%q, %v) = %q, want %q", tt.sqlType, tt.notNull, result, tt.want) } }) } } func TestTypeMapper_BuildBunTag(t *testing.T) { mapper := NewTypeMapper() tests := []struct { name string column *models.Column want []string // Parts that should be in the tag }{ { name: "primary key", column: &models.Column{ Name: "id", Type: "bigint", IsPrimaryKey: true, NotNull: true, }, want: []string{"id,", "type:bigint,", "pk,"}, }, { name: "nullable varchar", column: &models.Column{ Name: "email", Type: "varchar", Length: 255, NotNull: false, }, want: []string{"email,", "type:varchar(255),", "nullzero,"}, }, { name: "with default", column: &models.Column{ Name: "status", Type: "text", NotNull: true, Default: "active", }, want: []string{"status,", "type:text,", "default:active,"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := mapper.BuildBunTag(tt.column, nil) for _, part := range tt.want { if !strings.Contains(result, part) { t.Errorf("BuildBunTag() = %q, missing %q", result, part) } } }) } }