226 lines
6.5 KiB
Go
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
|
|
}
|