From 120ffc6a5a2aa7d8c37f33cebe91eab2cf452482 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 10 Jan 2026 13:49:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(writer):=20=F0=9F=8E=89=20Update=20relatio?= =?UTF-8?q?nship=20field=20naming=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor generateRelationshipFieldName to use foreign key columns for unique naming. * Add test for multiple references to the same table to ensure unique relationship field names. * Update existing tests to reflect new naming convention. --- pkg/writers/bun/writer.go | 21 +++++-- pkg/writers/bun/writer_test.go | 108 ++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/pkg/writers/bun/writer.go b/pkg/writers/bun/writer.go index a60daf5..aaa6c48 100644 --- a/pkg/writers/bun/writer.go +++ b/pkg/writers/bun/writer.go @@ -239,7 +239,7 @@ 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.ReferencedTable) + fieldName := w.generateRelationshipFieldName(constraint) relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-one") modelData.AddRelationshipField(&FieldData{ @@ -267,7 +267,7 @@ 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.generateRelationshipFieldName(constraint) + "s" // Pluralize relationTag := w.typeMapper.BuildRelationshipTag(constraint, "has-many") modelData.AddRelationshipField(&FieldData{ @@ -310,10 +310,19 @@ 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) +// generateRelationshipFieldName generates a field name for a relationship based on the foreign key column +func (w *Writer) generateRelationshipFieldName(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" + return "Rel" + SnakeCaseToPascalCase(columnName) + } + + // Fallback to table-based prefix if no columns defined + return "Rel" + GeneratePrefix(constraint.ReferencedTable) } // getPackageName returns the package name from options or defaults to "models" diff --git a/pkg/writers/bun/writer_test.go b/pkg/writers/bun/writer_test.go index 9a17b53..a643b2a 100644 --- a/pkg/writers/bun/writer_test.go +++ b/pkg/writers/bun/writer_test.go @@ -175,14 +175,118 @@ func TestWriter_WriteDatabase_MultiFile(t *testing.T) { postsStr := string(postsContent) // Verify relationship is present with Bun format - if !strings.Contains(postsStr, "USE") { - t.Errorf("Missing relationship field USE") + // Should now be RelUserID 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) } } +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") + } +} + func TestTypeMapper_SQLTypeToGoType_Bun(t *testing.T) { mapper := NewTypeMapper()