package orm import ( "fmt" "reflect" "strings" ) type RelationshipType int const ( HasOne RelationshipType = iota HasMany ManyToMany ) type Relationship struct { Type RelationshipType Model *Model FieldName string Idx int RelatedType reflect.Type RelatedModel *Model Kind reflect.Kind // field kind (struct, slice, ...) m2mInverse *Relationship } func (r *Relationship) JoinTable() string { return r.Model.TableName + "_" + r.RelatedModel.TableName } func (r *Relationship) JoinField() string { isMany := r.Type == HasMany //|| r.Type == ManyToMany if isMany && r.Model.embeddedIsh { return r.Model.Name + r.FieldName + "ID" } else if isMany && !r.Model.embeddedIsh && r.m2mInverse == nil { return r.Model.Name + "ID" } else if r.Type == ManyToMany && !r.Model.embeddedIsh { return r.RelatedModel.Name + "ID" } return r.FieldName + "ID" } func (r *Relationship) aliasThingy() string { return pascalToSnakeCase(r.Model.Name + "." + r.FieldName) } func (r *Relationship) relatedAlias() string { return pascalToSnakeCase(r.RelatedModel.Name + "." + r.FieldName) } func (r *Relationship) m2mIsh() bool { return r.Model.embeddedIsh && !r.RelatedModel.embeddedIsh && 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.JoinTable() jargs := make([]any, 0) jcols := make([]string, 0) jcols = append(jcols, fmt.Sprintf("%s_%s", r.Model.TableName, pascalToSnakeCase(r.FieldName), )) 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.JoinTable(), 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, ", ")) fmt.Printf("[INSERT/JOIN] %s { %s }\n", jsql, logTrunc(jargs, 200)) if !e.engine.dryRun { _ = e.tx.QueryRow(e.ctx, jsql, jargs...).Scan() } } return nil } func (r *Relationship) joinDelete(pk, fk any, q *Query) error { jc := fmt.Sprintf("%s_%s", r.Model.TableName, pascalToSnakeCase(r.FieldName)) ds := fmt.Sprintf("DELETE FROM %s where %s = $1 and %s = $2", r.JoinTable(), jc, r.RelatedModel.TableName+"_id") fmt.Printf("[DELETE/JOIN] %s { %s }\n", ds, logTrunc([]any{pk, fk}, 200)) if !q.engine.dryRun { _, err := q.tx.Exec(q.ctx, ds, pk, fk) return err } return nil } func parseRelationship(field reflect.StructField, modelMap map[string]*Model, outerType reflect.Type, idx int) *Relationship { rel := &Relationship{ Model: modelMap[outerType.Name()], RelatedModel: modelMap[field.Type.Name()], RelatedType: field.Type, Idx: idx, Kind: field.Type.Kind(), FieldName: field.Name, } 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.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: rel.Type = HasMany } return rel } func addForeignKeyFields(ref *Relationship) { rf := ref.RelatedModel.Fields[ref.RelatedModel.IDField] if rf != nil { if !ref.RelatedModel.embeddedIsh && !ref.Model.embeddedIsh { ff := ref.Model.Fields[ref.FieldName] ff.ColumnType = rf.ColumnType ff.ColumnName = pascalToSnakeCase(ref.JoinField()) ff.isForeignKey = true ff.fk = ref } else if !ref.Model.embeddedIsh { sid := strings.TrimSuffix(ref.JoinField(), "ID") ref.RelatedModel.Relationships[sid] = &Relationship{ FieldName: sid, Type: HasOne, RelatedModel: ref.Model, Model: ref.RelatedModel, Kind: ref.RelatedModel.Type.Kind(), Idx: -1, RelatedType: ref.Model.Type, } ref.RelatedModel.addField(&Field{ ColumnType: rf.ColumnType, ColumnName: pascalToSnakeCase(ref.JoinField()), Name: sid, isForeignKey: true, Type: rf.Type, Index: -1, fk: ref.RelatedModel.Relationships[sid], }) } else if ref.Model.embeddedIsh && !ref.RelatedModel.embeddedIsh { } } else { ref.RelatedModel.addField(&Field{ Name: "ID", ColumnName: "id", ColumnType: "bigserial", PrimaryKey: true, Type: ref.RelatedType, Index: -1, AutoIncrement: true, }) ff := ref.Model.Fields[ref.FieldName] ff.ColumnType = "bigint" ff.ColumnName = pascalToSnakeCase(ref.RelatedModel.Name + "ID") ff.isForeignKey = true ff.fk = ref ref.RelatedModel.IDField = "ID" /* nn := ref.Model.Name + "ID" ref.RelatedModel.Relationships[ref.Model.Name] = &Relationship{ Type: HasOne, RelatedModel: ref.Model, Model: ref.RelatedModel, Idx: 65536, Kind: ref.RelatedModel.Type.Kind(), RelatedType: ref.RelatedModel.Type, FieldName: nn, } ref.RelatedModel.addField(&Field{ Name: nn, Type: ref.Model.Type, fk: ref.RelatedModel.Relationships[nn], ColumnName: pascalToSnakeCase(nn), Index: -1, ColumnType: ref.Model.Fields[ref.Model.IDField].ColumnType, isForeignKey: true, })*/ } }