Files
relspecgo/pkg/readers/graphql/relationships.go
2025-12-28 11:41:55 +02:00

226 lines
6.5 KiB
Go

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
}