From 4181cb1fbdcb5186cd51383d4c9fee014585408e Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 10 Jan 2026 17:45:13 +0200 Subject: [PATCH] =?UTF-8?q?feat(writer):=20=F0=9F=8E=89=20Enhance=20relati?= =?UTF-8?q?onship=20field=20naming=20and=20uniqueness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update relationship field naming conventions for has-one and has-many relationships. * Implement logic to ensure unique field names by tracking used names. * Add tests to verify new naming conventions and uniqueness constraints. --- pkg/writers/bun/writer.go | 56 +++++- pkg/writers/bun/writer_test.go | 196 +++++++++++++++++++- pkg/writers/gorm/writer.go | 67 ++++++- pkg/writers/gorm/writer_test.go | 306 +++++++++++++++++++++++++++++++- 4 files changed, 610 insertions(+), 15 deletions(-) diff --git a/pkg/writers/bun/writer.go b/pkg/writers/bun/writer.go index aaa6c48..a7ba21f 100644 --- a/pkg/writers/bun/writer.go +++ b/pkg/writers/bun/writer.go @@ -225,6 +225,9 @@ func (w *Writer) writeMultiFile(db *models.Database) error { // addRelationshipFields adds relationship fields to the model based on foreign keys func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table, schema *models.Schema, db *models.Database) { + // Track used field names to detect duplicates + usedFieldNames := make(map[string]int) + // For each foreign key in this table, add a belongs-to/has-one relationship for _, constraint := range table.Constraints { if constraint.Type != models.ForeignKeyConstraint { @@ -239,7 +242,8 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table // Create relationship field (has-one in Bun, similar to belongs-to in GORM) refModelName := w.getModelName(constraint.ReferencedTable) - fieldName := w.generateRelationshipFieldName(constraint) + fieldName := w.generateHasOneFieldName(constraint) + fieldName = w.ensureUniqueFieldName(fieldName, usedFieldNames) relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-one") modelData.AddRelationshipField(&FieldData{ @@ -267,7 +271,8 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table if constraint.ReferencedTable == table.Name && constraint.ReferencedSchema == schema.Name { // Add has-many relationship otherModelName := w.getModelName(otherTable.Name) - fieldName := w.generateRelationshipFieldName(constraint) + "s" // Pluralize + fieldName := w.generateHasManyFieldName(constraint, otherTable.Name) + fieldName = w.ensureUniqueFieldName(fieldName, usedFieldNames) relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-many") modelData.AddRelationshipField(&FieldData{ @@ -310,14 +315,15 @@ func (w *Writer) getModelName(tableName string) string { return modelName } -// generateRelationshipFieldName generates a field name for a relationship based on the foreign key column -func (w *Writer) generateRelationshipFieldName(constraint *models.Constraint) string { +// generateHasOneFieldName generates a field name for has-one relationships +// Uses the foreign key column name for uniqueness +func (w *Writer) generateHasOneFieldName(constraint *models.Constraint) string { // Use the foreign key column name to ensure uniqueness // If there are multiple columns, use the first one if len(constraint.Columns) > 0 { columnName := constraint.Columns[0] // Convert to PascalCase for proper Go field naming - // e.g., "rid_filepointer_request" -> "RidFilepointerRequest" + // e.g., "rid_filepointer_request" -> "RelRIDFilepointerRequest" return "Rel" + SnakeCaseToPascalCase(columnName) } @@ -325,6 +331,46 @@ func (w *Writer) generateRelationshipFieldName(constraint *models.Constraint) st return "Rel" + GeneratePrefix(constraint.ReferencedTable) } +// generateHasManyFieldName generates a field name for has-many relationships +// Uses the foreign key column name + source table name to avoid duplicates +func (w *Writer) generateHasManyFieldName(constraint *models.Constraint, sourceTableName string) string { + // For has-many, we need to include the source table name to avoid duplicates + // e.g., multiple tables referencing the same column on this table + if len(constraint.Columns) > 0 { + columnName := constraint.Columns[0] + // Get the model name for the source table (pluralized) + sourceModelName := w.getModelName(sourceTableName) + // Remove "Model" prefix if present + sourceModelName = strings.TrimPrefix(sourceModelName, "Model") + + // Convert column to PascalCase and combine with source table + // e.g., "rid_api_provider" + "Login" -> "RelRIDAPIProviderLogins" + columnPart := SnakeCaseToPascalCase(columnName) + return "Rel" + columnPart + Pluralize(sourceModelName) + } + + // Fallback to table-based naming + sourceModelName := w.getModelName(sourceTableName) + sourceModelName = strings.TrimPrefix(sourceModelName, "Model") + return "Rel" + Pluralize(sourceModelName) +} + +// ensureUniqueFieldName ensures a field name is unique by adding numeric suffixes if needed +func (w *Writer) ensureUniqueFieldName(fieldName string, usedNames map[string]int) string { + originalName := fieldName + count := usedNames[originalName] + + if count > 0 { + // Name is already used, add numeric suffix + fieldName = fmt.Sprintf("%s%d", originalName, count+1) + } + + // Increment the counter for this base name + usedNames[originalName]++ + + return fieldName +} + // getPackageName returns the package name from options or defaults to "models" func (w *Writer) getPackageName() string { if w.options.PackageName != "" { diff --git a/pkg/writers/bun/writer_test.go b/pkg/writers/bun/writer_test.go index a643b2a..a5e86a9 100644 --- a/pkg/writers/bun/writer_test.go +++ b/pkg/writers/bun/writer_test.go @@ -175,13 +175,26 @@ func TestWriter_WriteDatabase_MultiFile(t *testing.T) { postsStr := string(postsContent) // Verify relationship is present with Bun format - // Should now be RelUserID instead of USE + // 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 RelUserIDPosts (has-many) field + if !strings.Contains(usersStr, "RelUserIDPosts") { + t.Errorf("Missing has-many relationship field RelUserIDPosts") + } } func TestWriter_MultipleReferencesToSameTable(t *testing.T) { @@ -285,6 +298,187 @@ func TestWriter_MultipleReferencesToSameTable(t *testing.T) { 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{ + "RelRIDFilepointerRequestAPIEvents", // Has many via rid_filepointer_request + "RelRIDFilepointerResponseAPIEvents", // 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{ + "RelRIDAPIProviderLogins", // Has many via Login + "RelRIDAPIProviderFilepointers", // Has many via Filepointer + "RelRIDAPIProviderAPIEvents", // 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 TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) { diff --git a/pkg/writers/gorm/writer.go b/pkg/writers/gorm/writer.go index ee232f4..2a03655 100644 --- a/pkg/writers/gorm/writer.go +++ b/pkg/writers/gorm/writer.go @@ -219,6 +219,9 @@ func (w *Writer) writeMultiFile(db *models.Database) error { // addRelationshipFields adds relationship fields to the model based on foreign keys func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table, schema *models.Schema, db *models.Database) { + // Track used field names to detect duplicates + usedFieldNames := make(map[string]int) + // For each foreign key in this table, add a belongs-to relationship for _, constraint := range table.Constraints { if constraint.Type != models.ForeignKeyConstraint { @@ -233,7 +236,8 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table // Create relationship field (belongs-to) refModelName := w.getModelName(constraint.ReferencedTable) - fieldName := w.generateRelationshipFieldName(constraint.ReferencedTable) + fieldName := w.generateBelongsToFieldName(constraint) + fieldName = w.ensureUniqueFieldName(fieldName, usedFieldNames) relationTag := w.typeMapper.BuildRelationshipTag(constraint, false) modelData.AddRelationshipField(&FieldData{ @@ -261,7 +265,8 @@ func (w *Writer) addRelationshipFields(modelData *ModelData, table *models.Table if constraint.ReferencedTable == table.Name && constraint.ReferencedSchema == schema.Name { // Add has-many relationship otherModelName := w.getModelName(otherTable.Name) - fieldName := w.generateRelationshipFieldName(otherTable.Name) + "s" // Pluralize + fieldName := w.generateHasManyFieldName(constraint, otherTable.Name) + fieldName = w.ensureUniqueFieldName(fieldName, usedFieldNames) relationTag := w.typeMapper.BuildRelationshipTag(constraint, true) modelData.AddRelationshipField(&FieldData{ @@ -304,10 +309,60 @@ func (w *Writer) getModelName(tableName string) string { return modelName } -// generateRelationshipFieldName generates a field name for a relationship -func (w *Writer) generateRelationshipFieldName(tableName string) string { - // Use just the prefix (3 letters) for relationship fields - return GeneratePrefix(tableName) +// generateBelongsToFieldName generates a field name for belongs-to relationships +// Uses the foreign key column name for uniqueness +func (w *Writer) generateBelongsToFieldName(constraint *models.Constraint) string { + // Use the foreign key column name to ensure uniqueness + // If there are multiple columns, use the first one + if len(constraint.Columns) > 0 { + columnName := constraint.Columns[0] + // Convert to PascalCase for proper Go field naming + // e.g., "rid_filepointer_request" -> "RelRIDFilepointerRequest" + return "Rel" + SnakeCaseToPascalCase(columnName) + } + + // Fallback to table-based prefix if no columns defined + return "Rel" + GeneratePrefix(constraint.ReferencedTable) +} + +// generateHasManyFieldName generates a field name for has-many relationships +// Uses the foreign key column name + source table name to avoid duplicates +func (w *Writer) generateHasManyFieldName(constraint *models.Constraint, sourceTableName string) string { + // For has-many, we need to include the source table name to avoid duplicates + // e.g., multiple tables referencing the same column on this table + if len(constraint.Columns) > 0 { + columnName := constraint.Columns[0] + // Get the model name for the source table (pluralized) + sourceModelName := w.getModelName(sourceTableName) + // Remove "Model" prefix if present + sourceModelName = strings.TrimPrefix(sourceModelName, "Model") + + // Convert column to PascalCase and combine with source table + // e.g., "rid_api_provider" + "Login" -> "RelRIDAPIProviderLogins" + columnPart := SnakeCaseToPascalCase(columnName) + return "Rel" + columnPart + Pluralize(sourceModelName) + } + + // Fallback to table-based naming + sourceModelName := w.getModelName(sourceTableName) + sourceModelName = strings.TrimPrefix(sourceModelName, "Model") + return "Rel" + Pluralize(sourceModelName) +} + +// ensureUniqueFieldName ensures a field name is unique by adding numeric suffixes if needed +func (w *Writer) ensureUniqueFieldName(fieldName string, usedNames map[string]int) string { + originalName := fieldName + count := usedNames[originalName] + + if count > 0 { + // Name is already used, add numeric suffix + fieldName = fmt.Sprintf("%s%d", originalName, count+1) + } + + // Increment the counter for this base name + usedNames[originalName]++ + + return fieldName } // getPackageName returns the package name from options or defaults to "models" diff --git a/pkg/writers/gorm/writer_test.go b/pkg/writers/gorm/writer_test.go index 0e2ba32..ef3f90b 100644 --- a/pkg/writers/gorm/writer_test.go +++ b/pkg/writers/gorm/writer_test.go @@ -164,9 +164,309 @@ func TestWriter_WriteDatabase_MultiFile(t *testing.T) { t.Fatalf("Failed to read posts file: %v", err) } - if !strings.Contains(string(postsContent), "USE *ModelUser") { - // Relationship field should be present - t.Logf("Posts content:\n%s", string(postsContent)) + postsStr := string(postsContent) + + // Verify relationship is present with new naming convention + // Should now be RelUserID (belongs-to) instead of USE + if !strings.Contains(postsStr, "RelUserID") { + t.Errorf("Missing relationship field RelUserID (new naming convention)") + } + + // 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 RelUserIDPosts (has-many) field + if !strings.Contains(usersStr, "RelUserIDPosts") { + t.Errorf("Missing has-many relationship field RelUserIDPosts") + } +} + +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", "foreignKey:RIDFilepointerRequest"}, + {"RelRIDFilepointerResponse", "foreignKey:RIDFilepointerResponse"}, + } + + 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{ + "RelRIDFilepointerRequestAPIEvents", // Has many via rid_filepointer_request + "RelRIDFilepointerResponseAPIEvents", // 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{ + "RelRIDAPIProviderLogins", // Has many via Login + "RelRIDAPIProviderFilepointers", // Has many via Filepointer + "RelRIDAPIProviderAPIEvents", // Has many via APIEvent + "RelRIDOwner", // Belongs to 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") } }