package graphql import ( "fmt" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" ) func (r *Reader) detectAndCreateRelationships(schema *models.Schema, ctx *parseContext) error { // Build table lookup map tableMap := make(map[string]*models.Table) for _, table := range schema.Tables { tableMap[table.Name] = table } // Process each table's relation fields for _, table := range schema.Tables { relationFields, ok := table.Metadata["relationFields"].(map[string]*fieldInfo) if !ok || len(relationFields) == 0 { continue } for fieldName, fieldInfo := range relationFields { targetTable, exists := tableMap[fieldInfo.typeName] if !exists { // Referenced type doesn't exist - might be an interface/union, skip continue } if fieldInfo.isArray { // This is a one-to-many or many-to-many reverse side // Check if target table has a reverse array field if r.hasReverseArrayField(targetTable, table.Name) { // Bidirectional array = many-to-many // Only create join table once (lexicographically first table creates it) if table.Name < targetTable.Name { if err := r.createManyToManyJoinTable(schema, table, targetTable, fieldName, tableMap); err != nil { return err } } } // For one-to-many, no action needed (FK is on the other table) } else { // This is a many-to-one or one-to-one // Create FK column on this table if err := r.createForeignKeyColumn(table, targetTable, fieldName, fieldInfo.isNullable, schema); err != nil { return err } } } } // Clean up metadata for _, table := range schema.Tables { delete(table.Metadata, "relationFields") } return nil } func (r *Reader) hasReverseArrayField(table *models.Table, targetTypeName string) bool { relationFields, ok := table.Metadata["relationFields"].(map[string]*fieldInfo) if !ok { return false } for _, fieldInfo := range relationFields { if fieldInfo.typeName == targetTypeName && fieldInfo.isArray { return true } } return false } func (r *Reader) createForeignKeyColumn(fromTable, toTable *models.Table, fieldName string, nullable bool, schema *models.Schema) error { // Get primary key from target table pkCol := toTable.GetPrimaryKey() if pkCol == nil { return fmt.Errorf("target table %s has no primary key for relationship", toTable.Name) } // Create FK column name: {fieldName}Id fkColName := fieldName + "Id" // Check if column already exists (shouldn't happen but be safe) if _, exists := fromTable.Columns[fkColName]; exists { return nil } // Create FK column fkCol := models.InitColumn(fkColName, fromTable.Name, schema.Name) fkCol.Type = pkCol.Type fkCol.NotNull = !nullable fromTable.Columns[fkColName] = fkCol // Create FK constraint constraint := models.InitConstraint( fmt.Sprintf("fk_%s_%s", fromTable.Name, fieldName), models.ForeignKeyConstraint, ) constraint.Schema = schema.Name constraint.Table = fromTable.Name constraint.Columns = []string{fkColName} constraint.ReferencedSchema = schema.Name constraint.ReferencedTable = toTable.Name constraint.ReferencedColumns = []string{pkCol.Name} constraint.OnDelete = "CASCADE" constraint.OnUpdate = "RESTRICT" fromTable.Constraints[constraint.Name] = constraint // Create relationship relationship := models.InitRelationship( fmt.Sprintf("rel_%s_%s", fromTable.Name, fieldName), models.OneToMany, ) relationship.FromTable = fromTable.Name relationship.FromSchema = schema.Name relationship.FromColumns = []string{fkColName} relationship.ToTable = toTable.Name relationship.ToSchema = schema.Name relationship.ToColumns = []string{pkCol.Name} relationship.ForeignKey = constraint.Name fromTable.Relationships[relationship.Name] = relationship return nil } func (r *Reader) createManyToManyJoinTable(schema *models.Schema, table1, table2 *models.Table, fieldName string, tableMap map[string]*models.Table) error { // Create join table name joinTableName := table1.Name + table2.Name // Check if join table already exists if _, exists := tableMap[joinTableName]; exists { return nil } // Get primary keys pk1 := table1.GetPrimaryKey() pk2 := table2.GetPrimaryKey() if pk1 == nil || pk2 == nil { return fmt.Errorf("cannot create many-to-many: tables must have primary keys") } // Create join table joinTable := models.InitTable(joinTableName, schema.Name) // Create FK column for table1 fkCol1Name := strings.ToLower(table1.Name) + "Id" fkCol1 := models.InitColumn(fkCol1Name, joinTable.Name, schema.Name) fkCol1.Type = pk1.Type fkCol1.NotNull = true joinTable.Columns[fkCol1Name] = fkCol1 // Create FK column for table2 fkCol2Name := strings.ToLower(table2.Name) + "Id" fkCol2 := models.InitColumn(fkCol2Name, joinTable.Name, schema.Name) fkCol2.Type = pk2.Type fkCol2.NotNull = true joinTable.Columns[fkCol2Name] = fkCol2 // Create composite primary key pkConstraint := models.InitConstraint( fmt.Sprintf("pk_%s", joinTableName), models.PrimaryKeyConstraint, ) pkConstraint.Schema = schema.Name pkConstraint.Table = joinTable.Name pkConstraint.Columns = []string{fkCol1Name, fkCol2Name} joinTable.Constraints[pkConstraint.Name] = pkConstraint // Create FK constraint to table1 fk1 := models.InitConstraint( fmt.Sprintf("fk_%s_%s", joinTableName, table1.Name), models.ForeignKeyConstraint, ) fk1.Schema = schema.Name fk1.Table = joinTable.Name fk1.Columns = []string{fkCol1Name} fk1.ReferencedSchema = schema.Name fk1.ReferencedTable = table1.Name fk1.ReferencedColumns = []string{pk1.Name} fk1.OnDelete = "CASCADE" fk1.OnUpdate = "RESTRICT" joinTable.Constraints[fk1.Name] = fk1 // Create FK constraint to table2 fk2 := models.InitConstraint( fmt.Sprintf("fk_%s_%s", joinTableName, table2.Name), models.ForeignKeyConstraint, ) fk2.Schema = schema.Name fk2.Table = joinTable.Name fk2.Columns = []string{fkCol2Name} fk2.ReferencedSchema = schema.Name fk2.ReferencedTable = table2.Name fk2.ReferencedColumns = []string{pk2.Name} fk2.OnDelete = "CASCADE" fk2.OnUpdate = "RESTRICT" joinTable.Constraints[fk2.Name] = fk2 // Create relationships rel1 := models.InitRelationship( fmt.Sprintf("rel_%s_%s_%s", joinTableName, table1.Name, table2.Name), models.ManyToMany, ) rel1.FromTable = table1.Name rel1.FromSchema = schema.Name rel1.ToTable = table2.Name rel1.ToSchema = schema.Name rel1.ThroughTable = joinTableName rel1.ThroughSchema = schema.Name joinTable.Relationships[rel1.Name] = rel1 // Add join table to schema schema.Tables = append(schema.Tables, joinTable) tableMap[joinTableName] = joinTable return nil }