package orm import ( "fmt" sb "github.com/henvic/pgq" "reflect" "strings" ) // RelationshipType - used to distinguish a Relationship between various // common entity relationship types type RelationshipType int const ( HasOne RelationshipType = iota HasMany BelongsTo ManyToOne // the other side of a HasMany relationship ManyToMany ) // Relationship - intermediate representation of how two types // relate to each other. i.e., if struct A embeds struct B, // a Relationship will be created for those two while parsing the Model for A. type Relationship struct { Type RelationshipType // the type of this relationship (see RelationshipType) JoinTable string // the name of the join table, if specified explicitly via struct tag, otherwise blank Model *Model // the primary Model which contains this relationship FieldName string // the name of the struct field with this relationship Idx int // the index of the struct field with this relationship RelatedType reflect.Type // the reflect.Type for the struct field named by FieldName RelatedModel *Model // the Model representing the type of the embedded slice/struct Kind reflect.Kind // field kind (struct, slice, ...) m2mInverse *Relationship // the "inverse" side of an explicit ManyToMany relationship Nullable bool // whether the foreign key for this relationship can have a nullable column OriginalField reflect.StructField // the original reflect.StructField object associated with this relationship } // ComputeJoinTable - computes the name of the join table for ManyToMany relationships. // will return a snake_cased autogenerated name for unidirectional ManyToMany . // the "implicit" behavior is invoked upon one of the following conditions being met: // - the primary Relationship.Model func (r *Relationship) ComputeJoinTable() string { if r.JoinTable != "" { return r.JoinTable } otherSide := r.RelatedModel.TableName if r.Model.embeddedIsh { otherSide = pascalToSnakeCase(r.FieldName) } return r.Model.TableName + "_" + otherSide } func (r *Relationship) relatedID() *Field { return r.RelatedModel.Fields[r.RelatedModel.IDField] } func (r *Relationship) primaryID() *Field { return r.Model.Fields[r.Model.IDField] } func (r *Relationship) joinField() string { if r.Type == ManyToOne { return r.RelatedModel.Name + "ID" } if r.Type == ManyToMany && !r.Model.embeddedIsh { return r.RelatedModel.Name + "ID" } return r.FieldName + "ID" } func (r *Relationship) m2mIsh() bool { needsMany := false if !r.Model.embeddedIsh && r.RelatedModel.embeddedIsh { rr, ok := r.RelatedModel.Relationships[r.Model.Name] if ok && rr.Type != ManyToOne { needsMany = true } } return ((r.Model.embeddedIsh && !r.RelatedModel.embeddedIsh) || needsMany) && r.Type == HasMany } func (r *Relationship) joinInsert(v reflect.Value, e *Query, pfk any) error { if r.Type != ManyToMany && !r.m2mIsh() { return nil } ichild := v for ichild.Kind() == reflect.Ptr { ichild = ichild.Elem() } if ichild.Kind() == reflect.Struct { jtable := r.ComputeJoinTable() jargs := make([]any, 0) jcols := make([]string, 0) jcols = append(jcols, fmt.Sprintf("%s_id", r.Model.TableName, )) jargs = append(jargs, pfk) jcols = append(jcols, r.RelatedModel.TableName+"_id") jargs = append(jargs, ichild.FieldByName(r.RelatedModel.IDField).Interface()) var ecnt int e.tx.QueryRow(e.ctx, fmt.Sprintf("SELECT count(*) from %s where %s = $1 and %s = $2", r.ComputeJoinTable(), jcols[0], jcols[1]), jargs...).Scan(&ecnt) if ecnt > 0 { return nil } jsql := fmt.Sprintf("INSERT INTO %s (%s) VALUES ($1, $2)", jtable, strings.Join(jcols, ", ")) e.engine.logQuery("insert/join", jsql, jargs) if !e.engine.dryRun { _ = e.tx.QueryRow(e.ctx, jsql, jargs...).Scan() } } return nil } func (r *Relationship) joinDelete(pk, fk any, q *Query) error { dq := sb.Delete(r.ComputeJoinTable()).Where(fmt.Sprintf("%s_id = ?", r.Model.TableName), pk) if fk != nil { dq = dq.Where(fmt.Sprintf("%s_id = ?", r.RelatedModel.TableName), fk) } ds, aa := dq.MustSQL() q.engine.logQuery("delete/join", ds, aa) if !q.engine.dryRun { _, err := q.tx.Exec(q.ctx, ds, aa...) return err } return nil } func parseRelationship(field reflect.StructField, modelMap map[string]*Model, outerType reflect.Type, idx int, settings map[string]string) *Relationship { rel := &Relationship{ Model: modelMap[outerType.Name()], RelatedModel: modelMap[field.Type.Name()], RelatedType: field.Type, Idx: idx, Kind: field.Type.Kind(), FieldName: field.Name, OriginalField: field, } if rel.RelatedType.Kind() == reflect.Slice || rel.RelatedType.Kind() == reflect.Array { rel.RelatedType = rel.RelatedType.Elem() } if rel.RelatedModel == nil { if rel.RelatedType.Name() == "" { rt := rel.RelatedType for rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Slice || rt.Kind() == reflect.Array { rel.Nullable = true rel.RelatedType = rel.RelatedType.Elem() rt = rel.RelatedType } } rel.RelatedModel = modelMap[rel.RelatedType.Name()] if _, ok := modelMap[rel.RelatedType.Name()]; !ok { rel.RelatedModel = parseModel(reflect.New(rel.RelatedType).Interface()) modelMap[rel.RelatedType.Name()] = rel.RelatedModel parseModelFields(rel.RelatedModel, modelMap) rel.RelatedModel.embeddedIsh = true } } switch field.Type.Kind() { case reflect.Struct: rel.Type = HasOne case reflect.Slice, reflect.Array: rel.Type = HasMany } maybeM2m := settings["m2m"] if maybeM2m == "" { maybeM2m = settings["manytomany"] } if rel.Type == HasMany && maybeM2m != "" { rel.JoinTable = maybeM2m } return rel } func addForeignKeyFields(ref *Relationship) { if !ref.RelatedModel.embeddedIsh && !ref.Model.embeddedIsh { ref.Type = BelongsTo } else if !ref.Model.embeddedIsh && ref.RelatedModel.embeddedIsh { if ref.Type == HasMany { nr := &Relationship{ RelatedModel: ref.Model, Model: ref.RelatedModel, Kind: ref.RelatedModel.Type.Kind(), Idx: -1, RelatedType: ref.Model.Type, } nr.Type = ManyToOne nr.FieldName = nr.RelatedModel.Name ref.RelatedModel.Relationships[nr.FieldName] = nr } else if ref.Type == HasOne { ref.Type = BelongsTo } } else if ref.Model.embeddedIsh && !ref.RelatedModel.embeddedIsh { } }