package gorm import ( "context" "database/sql" "errors" "fmt" "reflect" "sort" "strings" "gorm.io/gorm/clause" "gorm.io/gorm/logger" "gorm.io/gorm/schema" ) type result struct { Result sql.Result RowsAffected int64 } func (info *result) ModifyStatement(stmt *Statement) { stmt.Result = info } // Build implements clause.Expression interface func (result) Build(clause.Builder) { } func WithResult() *result { return &result{} } type Interface[T any] interface { Raw(sql string, values ...interface{}) ExecInterface[T] Exec(ctx context.Context, sql string, values ...interface{}) error CreateInterface[T] } type CreateInterface[T any] interface { ExecInterface[T] // chain methods available at start; Select/Omit keep CreateInterface to allow Create chaining Scopes(scopes ...func(db *Statement)) ChainInterface[T] Where(query interface{}, args ...interface{}) ChainInterface[T] Not(query interface{}, args ...interface{}) ChainInterface[T] Or(query interface{}, args ...interface{}) ChainInterface[T] Limit(offset int) ChainInterface[T] Offset(offset int) ChainInterface[T] Joins(query clause.JoinTarget, on func(db JoinBuilder, joinTable clause.Table, curTable clause.Table) error) ChainInterface[T] Preload(association string, query func(db PreloadBuilder) error) ChainInterface[T] Select(query string, args ...interface{}) CreateInterface[T] Omit(columns ...string) CreateInterface[T] MapColumns(m map[string]string) ChainInterface[T] Distinct(args ...interface{}) ChainInterface[T] Group(name string) ChainInterface[T] Having(query interface{}, args ...interface{}) ChainInterface[T] Order(value interface{}) ChainInterface[T] Build(builder clause.Builder) Delete(ctx context.Context) (rowsAffected int, err error) Update(ctx context.Context, name string, value any) (rowsAffected int, err error) Updates(ctx context.Context, t T) (rowsAffected int, err error) Count(ctx context.Context, column string) (result int64, err error) Table(name string, args ...interface{}) CreateInterface[T] Create(ctx context.Context, r *T) error CreateInBatches(ctx context.Context, r *[]T, batchSize int) error Set(assignments ...clause.Assigner) SetCreateOrUpdateInterface[T] } type ChainInterface[T any] interface { ExecInterface[T] Scopes(scopes ...func(db *Statement)) ChainInterface[T] Where(query interface{}, args ...interface{}) ChainInterface[T] Not(query interface{}, args ...interface{}) ChainInterface[T] Or(query interface{}, args ...interface{}) ChainInterface[T] Limit(offset int) ChainInterface[T] Offset(offset int) ChainInterface[T] Joins(query clause.JoinTarget, on func(db JoinBuilder, joinTable clause.Table, curTable clause.Table) error) ChainInterface[T] Preload(association string, query func(db PreloadBuilder) error) ChainInterface[T] Select(query string, args ...interface{}) ChainInterface[T] Omit(columns ...string) ChainInterface[T] MapColumns(m map[string]string) ChainInterface[T] Distinct(args ...interface{}) ChainInterface[T] Group(name string) ChainInterface[T] Having(query interface{}, args ...interface{}) ChainInterface[T] Order(value interface{}) ChainInterface[T] Set(assignments ...clause.Assigner) SetUpdateOnlyInterface[T] Build(builder clause.Builder) Table(name string, args ...interface{}) ChainInterface[T] Delete(ctx context.Context) (rowsAffected int, err error) Update(ctx context.Context, name string, value any) (rowsAffected int, err error) Updates(ctx context.Context, t T) (rowsAffected int, err error) Count(ctx context.Context, column string) (result int64, err error) } // SetUpdateOnlyInterface is returned by Set after chaining; only Update is allowed type SetUpdateOnlyInterface[T any] interface { Update(ctx context.Context) (rowsAffected int, err error) } // SetCreateOrUpdateInterface is returned by Set at start; Create or Update are allowed type SetCreateOrUpdateInterface[T any] interface { Create(ctx context.Context) error Update(ctx context.Context) (rowsAffected int, err error) } type ExecInterface[T any] interface { Scan(ctx context.Context, r interface{}) error First(context.Context) (T, error) Last(ctx context.Context) (T, error) Take(context.Context) (T, error) Find(ctx context.Context) ([]T, error) FindInBatches(ctx context.Context, batchSize int, fc func(data []T, batch int) error) error Row(ctx context.Context) *sql.Row Rows(ctx context.Context) (*sql.Rows, error) } type JoinBuilder interface { Select(...string) JoinBuilder Omit(...string) JoinBuilder Where(query interface{}, args ...interface{}) JoinBuilder Not(query interface{}, args ...interface{}) JoinBuilder Or(query interface{}, args ...interface{}) JoinBuilder } type PreloadBuilder interface { Select(...string) PreloadBuilder Omit(...string) PreloadBuilder Where(query interface{}, args ...interface{}) PreloadBuilder Not(query interface{}, args ...interface{}) PreloadBuilder Or(query interface{}, args ...interface{}) PreloadBuilder Limit(offset int) PreloadBuilder Offset(offset int) PreloadBuilder Order(value interface{}) PreloadBuilder LimitPerRecord(num int) PreloadBuilder } type op func(*DB) *DB func G[T any](db *DB, opts ...clause.Expression) Interface[T] { v := &g[T]{ db: db, ops: make([]op, 0, 5), } if len(opts) > 0 { v.ops = append(v.ops, func(db *DB) *DB { return db.Clauses(opts...) }) } v.createG = &createG[T]{ chainG: chainG[T]{ execG: execG[T]{g: v}, }, } return v } type g[T any] struct { *createG[T] db *DB ops []op } func (g *g[T]) apply(ctx context.Context) *DB { db := g.db if !db.DryRun { db = db.Session(&Session{NewDB: true, Context: ctx}).getInstance() } for _, op := range g.ops { db = op(db) } return db } func (c *g[T]) Raw(sql string, values ...interface{}) ExecInterface[T] { return execG[T]{g: &g[T]{ db: c.db, ops: append(c.ops, func(db *DB) *DB { var r T return db.Model(r).Raw(sql, values...) }), }} } func (c *g[T]) Exec(ctx context.Context, sql string, values ...interface{}) error { var r T return c.apply(ctx).Model(r).Exec(sql, values...).Error } type createG[T any] struct { chainG[T] } func (c createG[T]) Table(name string, args ...interface{}) CreateInterface[T] { return createG[T]{c.with(func(db *DB) *DB { return db.Table(name, args...) })} } func (c createG[T]) Select(query string, args ...interface{}) CreateInterface[T] { return createG[T]{c.with(func(db *DB) *DB { return db.Select(query, args...) })} } func (c createG[T]) Omit(columns ...string) CreateInterface[T] { return createG[T]{c.with(func(db *DB) *DB { return db.Omit(columns...) })} } func (c createG[T]) Set(assignments ...clause.Assigner) SetCreateOrUpdateInterface[T] { return c.processSet(assignments...) } func (c createG[T]) Create(ctx context.Context, r *T) error { return c.g.apply(ctx).Create(r).Error } func (c createG[T]) CreateInBatches(ctx context.Context, r *[]T, batchSize int) error { return c.g.apply(ctx).CreateInBatches(r, batchSize).Error } type chainG[T any] struct { execG[T] } func (c chainG[T]) getInstance() *DB { var r T return c.g.apply(context.Background()).Model(r).getInstance() } func (c chainG[T]) with(v op) chainG[T] { return chainG[T]{ execG: execG[T]{g: &g[T]{ db: c.g.db, ops: append(append([]op(nil), c.g.ops...), v), }}, } } func (c chainG[T]) Table(name string, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Table(name, args...) }) } func (c chainG[T]) Scopes(scopes ...func(db *Statement)) ChainInterface[T] { return c.with(func(db *DB) *DB { for _, fc := range scopes { fc(db.Statement) } return db }) } func (c chainG[T]) Where(query interface{}, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Where(query, args...) }) } func (c chainG[T]) Not(query interface{}, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Not(query, args...) }) } func (c chainG[T]) Or(query interface{}, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Or(query, args...) }) } func (c chainG[T]) Limit(offset int) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Limit(offset) }) } func (c chainG[T]) Offset(offset int) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Offset(offset) }) } type joinBuilder struct { db *DB } func (q *joinBuilder) Where(query interface{}, args ...interface{}) JoinBuilder { q.db.Where(query, args...) return q } func (q *joinBuilder) Or(query interface{}, args ...interface{}) JoinBuilder { q.db.Where(query, args...) return q } func (q *joinBuilder) Not(query interface{}, args ...interface{}) JoinBuilder { q.db.Where(query, args...) return q } func (q *joinBuilder) Select(columns ...string) JoinBuilder { q.db.Select(columns) return q } func (q *joinBuilder) Omit(columns ...string) JoinBuilder { q.db.Omit(columns...) return q } type preloadBuilder struct { limitPerRecord int db *DB } func (q *preloadBuilder) Where(query interface{}, args ...interface{}) PreloadBuilder { q.db.Where(query, args...) return q } func (q *preloadBuilder) Or(query interface{}, args ...interface{}) PreloadBuilder { q.db.Where(query, args...) return q } func (q *preloadBuilder) Not(query interface{}, args ...interface{}) PreloadBuilder { q.db.Where(query, args...) return q } func (q *preloadBuilder) Select(columns ...string) PreloadBuilder { q.db.Select(columns) return q } func (q *preloadBuilder) Omit(columns ...string) PreloadBuilder { q.db.Omit(columns...) return q } func (q *preloadBuilder) Limit(limit int) PreloadBuilder { q.db.Limit(limit) return q } func (q *preloadBuilder) Offset(offset int) PreloadBuilder { q.db.Offset(offset) return q } func (q *preloadBuilder) Order(value interface{}) PreloadBuilder { q.db.Order(value) return q } func (q *preloadBuilder) LimitPerRecord(num int) PreloadBuilder { q.limitPerRecord = num return q } func (c chainG[T]) Joins(jt clause.JoinTarget, on func(db JoinBuilder, joinTable clause.Table, curTable clause.Table) error) ChainInterface[T] { return c.with(func(db *DB) *DB { if jt.Table == "" { jt.Table = clause.JoinTable(strings.Split(jt.Association, ".")...).Name } q := joinBuilder{db: db.Session(&Session{NewDB: true, Initialized: true}).Table(jt.Table)} if on != nil { if err := on(&q, clause.Table{Name: jt.Table}, clause.Table{Name: clause.CurrentTable}); err != nil { db.AddError(err) } } j := join{ Name: jt.Association, Alias: jt.Table, Selects: q.db.Statement.Selects, Omits: q.db.Statement.Omits, JoinType: jt.Type, } if where, ok := q.db.Statement.Clauses["WHERE"].Expression.(clause.Where); ok { j.On = &where } if jt.Subquery != nil { joinType := j.JoinType if joinType == "" { joinType = clause.LeftJoin } if db, ok := jt.Subquery.(interface{ getInstance() *DB }); ok { stmt := db.getInstance().Statement if len(j.Selects) == 0 { j.Selects = stmt.Selects } if len(j.Omits) == 0 { j.Omits = stmt.Omits } } expr := clause.NamedExpr{SQL: fmt.Sprintf("%s JOIN (?) AS ?", joinType), Vars: []interface{}{jt.Subquery, clause.Table{Name: j.Alias}}} if j.On != nil { expr.SQL += " ON ?" expr.Vars = append(expr.Vars, clause.AndConditions{Exprs: j.On.Exprs}) } j.Expression = expr } db.Statement.Joins = append(db.Statement.Joins, j) sort.Slice(db.Statement.Joins, func(i, j int) bool { return db.Statement.Joins[i].Name < db.Statement.Joins[j].Name }) return db }) } func (c chainG[T]) Select(query string, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Select(query, args...) }) } func (c chainG[T]) Omit(columns ...string) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Omit(columns...) }) } func (c chainG[T]) MapColumns(m map[string]string) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.MapColumns(m) }) } func (c chainG[T]) Set(assignments ...clause.Assigner) SetUpdateOnlyInterface[T] { return c.processSet(assignments...) } func (c chainG[T]) Distinct(args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Distinct(args...) }) } func (c chainG[T]) Group(name string) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Group(name) }) } func (c chainG[T]) Having(query interface{}, args ...interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Having(query, args...) }) } func (c chainG[T]) Order(value interface{}) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Order(value) }) } func (c chainG[T]) Preload(association string, query func(db PreloadBuilder) error) ChainInterface[T] { return c.with(func(db *DB) *DB { return db.Preload(association, func(tx *DB) *DB { q := preloadBuilder{db: tx.getInstance()} if query != nil { if err := query(&q); err != nil { db.AddError(err) } } relation, ok := db.Statement.Schema.Relationships.Relations[association] if !ok { if preloadFields := strings.Split(association, "."); len(preloadFields) > 1 { relationships := &db.Statement.Schema.Relationships for _, field := range preloadFields { var ok bool relation, ok = relationships.Relations[field] if ok { relationships = &relation.FieldSchema.Relationships } else { db.AddError(fmt.Errorf("relation %s not found", association)) return nil } } } else { db.AddError(fmt.Errorf("relation %s not found", association)) return nil } } if q.limitPerRecord > 0 { if relation.JoinTable != nil { tx.AddError(fmt.Errorf("many2many relation %s don't support LimitPerRecord", association)) return tx } refColumns := []clause.Column{} for _, rel := range relation.References { if rel.OwnPrimaryKey { refColumns = append(refColumns, clause.Column{Name: rel.ForeignKey.DBName}) } } if len(refColumns) != 0 { selectExpr := clause.CommaExpression{} for _, column := range q.db.Statement.Selects { selectExpr.Exprs = append(selectExpr.Exprs, clause.Expr{SQL: "?", Vars: []interface{}{clause.Column{Name: column}}}) } if len(selectExpr.Exprs) == 0 { selectExpr.Exprs = []clause.Expression{clause.Expr{SQL: "*", Vars: []interface{}{}}} } partitionBy := clause.CommaExpression{} for _, column := range refColumns { partitionBy.Exprs = append(partitionBy.Exprs, clause.Expr{SQL: "?", Vars: []interface{}{clause.Column{Name: column.Name}}}) } rnnColumn := clause.Column{Name: "gorm_preload_rnn"} sql := "ROW_NUMBER() OVER (PARTITION BY ? ?)" vars := []interface{}{partitionBy} if orderBy, ok := q.db.Statement.Clauses["ORDER BY"]; ok { vars = append(vars, orderBy) } else { vars = append(vars, clause.Clause{Name: "ORDER BY", Expression: clause.OrderBy{ Columns: []clause.OrderByColumn{{Column: clause.PrimaryColumn, Desc: true}}, }}) } vars = append(vars, rnnColumn) selectExpr.Exprs = append(selectExpr.Exprs, clause.Expr{SQL: sql + " AS ?", Vars: vars}) q.db.Clauses(clause.Select{Expression: selectExpr}) return q.db.Session(&Session{NewDB: true}).Unscoped().Table("(?) t", q.db).Where("? <= ?", rnnColumn, q.limitPerRecord) } } return q.db }) }) } func (c chainG[T]) Delete(ctx context.Context) (rowsAffected int, err error) { r := new(T) res := c.g.apply(ctx).Delete(r) return int(res.RowsAffected), res.Error } func (c chainG[T]) Update(ctx context.Context, name string, value any) (rowsAffected int, err error) { var r T res := c.g.apply(ctx).Model(r).Update(name, value) return int(res.RowsAffected), res.Error } func (c chainG[T]) Updates(ctx context.Context, t T) (rowsAffected int, err error) { res := c.g.apply(ctx).Updates(t) return int(res.RowsAffected), res.Error } func (c chainG[T]) Count(ctx context.Context, column string) (result int64, err error) { var r T err = c.g.apply(ctx).Model(r).Select(column).Count(&result).Error return } func (c chainG[T]) Build(builder clause.Builder) { subdb := c.getInstance() subdb.Logger = logger.Discard subdb.DryRun = true if stmt, ok := builder.(*Statement); ok { if subdb.Statement.SQL.Len() > 0 { var ( vars = subdb.Statement.Vars sql = subdb.Statement.SQL.String() ) subdb.Statement.Vars = make([]interface{}, 0, len(vars)) for _, vv := range vars { subdb.Statement.Vars = append(subdb.Statement.Vars, vv) bindvar := strings.Builder{} subdb.BindVarTo(&bindvar, subdb.Statement, vv) sql = strings.Replace(sql, bindvar.String(), "?", 1) } subdb.Statement.SQL.Reset() subdb.Statement.Vars = stmt.Vars if strings.Contains(sql, "@") { clause.NamedExpr{SQL: sql, Vars: vars}.Build(subdb.Statement) } else { clause.Expr{SQL: sql, Vars: vars}.Build(subdb.Statement) } } else { subdb.Statement.Vars = append(stmt.Vars, subdb.Statement.Vars...) subdb.callbacks.Query().Execute(subdb) } builder.WriteString(subdb.Statement.SQL.String()) stmt.Vars = subdb.Statement.Vars } } type execG[T any] struct { g *g[T] } func (g execG[T]) First(ctx context.Context) (T, error) { var r T err := g.g.apply(ctx).First(&r).Error return r, err } func (g execG[T]) Scan(ctx context.Context, result interface{}) error { var r T err := g.g.apply(ctx).Model(r).Find(result).Error return err } func (g execG[T]) Last(ctx context.Context) (T, error) { var r T err := g.g.apply(ctx).Last(&r).Error return r, err } func (g execG[T]) Take(ctx context.Context) (T, error) { var r T err := g.g.apply(ctx).Take(&r).Error return r, err } func (g execG[T]) Find(ctx context.Context) ([]T, error) { var r []T err := g.g.apply(ctx).Find(&r).Error return r, err } func (g execG[T]) FindInBatches(ctx context.Context, batchSize int, fc func(data []T, batch int) error) error { var data []T return g.g.apply(ctx).FindInBatches(&data, batchSize, func(tx *DB, batch int) error { return fc(data, batch) }).Error } func (g execG[T]) Row(ctx context.Context) *sql.Row { return g.g.apply(ctx).Row() } func (g execG[T]) Rows(ctx context.Context) (*sql.Rows, error) { return g.g.apply(ctx).Rows() } func (c chainG[T]) processSet(items ...clause.Assigner) setCreateOrUpdateG[T] { var ( assigns []clause.Assignment assocOps []clause.Association ) for _, item := range items { // Check if it's an AssociationAssigner if assocAssigner, ok := item.(clause.AssociationAssigner); ok { assocOps = append(assocOps, assocAssigner.AssociationAssignments()...) } else { assigns = append(assigns, item.Assignments()...) } } return setCreateOrUpdateG[T]{ c: c, assigns: assigns, assocOps: assocOps, } } // setCreateOrUpdateG[T] is a struct that holds operations to be executed in a batch. // It supports regular assignments and association operations. type setCreateOrUpdateG[T any] struct { c chainG[T] assigns []clause.Assignment assocOps []clause.Association } func (s setCreateOrUpdateG[T]) Update(ctx context.Context) (rowsAffected int, err error) { // Execute association operations for _, assocOp := range s.assocOps { if err := s.executeAssociationOperation(ctx, assocOp); err != nil { return 0, err } } // Execute assignment operations if len(s.assigns) > 0 { var r T res := s.c.g.apply(ctx).Model(r).Clauses(clause.Set(s.assigns)).Updates(map[string]interface{}{}) return int(res.RowsAffected), res.Error } return 0, nil } func (s setCreateOrUpdateG[T]) Create(ctx context.Context) error { // Execute association operations for _, assocOp := range s.assocOps { if err := s.executeAssociationOperation(ctx, assocOp); err != nil { return err } } // Execute assignment operations if len(s.assigns) > 0 { data := make(map[string]interface{}, len(s.assigns)) for _, a := range s.assigns { data[a.Column.Name] = a.Value } var r T return s.c.g.apply(ctx).Model(r).Create(data).Error } return nil } // executeAssociationOperation executes an association operation func (s setCreateOrUpdateG[T]) executeAssociationOperation(ctx context.Context, op clause.Association) error { var r T base := s.c.g.apply(ctx).Model(r) switch op.Type { case clause.OpCreate: return s.handleAssociationCreate(ctx, base, op) case clause.OpUnlink, clause.OpDelete, clause.OpUpdate: return s.handleAssociation(ctx, base, op) default: return fmt.Errorf("unknown association operation type: %v", op.Type) } } func (s setCreateOrUpdateG[T]) handleAssociationCreate(ctx context.Context, base *DB, op clause.Association) error { if len(op.Set) > 0 { return s.handleAssociationForOwners(base, ctx, func(owner T, assoc *Association) error { data := make(map[string]interface{}, len(op.Set)) for _, a := range op.Set { data[a.Column.Name] = a.Value } return assoc.Append(data) }, op.Association) } return s.handleAssociationForOwners(base, ctx, func(owner T, assoc *Association) error { return assoc.Append(op.Values...) }, op.Association) } // handleAssociationForOwners is a helper function that handles associations for all owners func (s setCreateOrUpdateG[T]) handleAssociationForOwners(base *DB, ctx context.Context, handler func(owner T, association *Association) error, associationName string) error { var owners []T if err := base.Find(&owners).Error; err != nil { return err } for _, owner := range owners { assoc := base.Session(&Session{NewDB: true, Context: ctx}).Model(&owner).Association(associationName) if assoc.Error != nil { return assoc.Error } if err := handler(owner, assoc); err != nil { return err } } return nil } func (s setCreateOrUpdateG[T]) handleAssociation(ctx context.Context, base *DB, op clause.Association) error { assoc := base.Association(op.Association) if assoc.Error != nil { return assoc.Error } var ( rel = assoc.Relationship assocModel = reflect.New(rel.FieldSchema.ModelType).Interface() fkNil = map[string]any{} setMap = make(map[string]any, len(op.Set)) ownerPKNames []string ownerFKNames []string primaryColumns []any foreignColumns []any ) for _, a := range op.Set { setMap[a.Column.Name] = a.Value } for _, ref := range rel.References { fkNil[ref.ForeignKey.DBName] = nil if ref.OwnPrimaryKey && ref.PrimaryKey != nil { ownerPKNames = append(ownerPKNames, ref.PrimaryKey.DBName) primaryColumns = append(primaryColumns, clause.Column{Name: ref.PrimaryKey.DBName}) foreignColumns = append(foreignColumns, clause.Column{Name: ref.ForeignKey.DBName}) } else if !ref.OwnPrimaryKey && ref.PrimaryKey != nil { ownerFKNames = append(ownerFKNames, ref.ForeignKey.DBName) primaryColumns = append(primaryColumns, clause.Column{Name: ref.PrimaryKey.DBName}) } } assocDB := s.c.g.db.Session(&Session{NewDB: true, Context: ctx}).Model(assocModel).Where(op.Conditions) switch rel.Type { case schema.HasOne, schema.HasMany: assocDB = assocDB.Where("? IN (?)", foreignColumns, base.Select(ownerPKNames)) switch op.Type { case clause.OpUnlink: return assocDB.Updates(fkNil).Error case clause.OpDelete: return assocDB.Delete(assocModel).Error case clause.OpUpdate: return assocDB.Updates(setMap).Error } case schema.BelongsTo: switch op.Type { case clause.OpDelete: return base.Transaction(func(tx *DB) error { assocDB.Statement.ConnPool = tx.Statement.ConnPool base.Statement.ConnPool = tx.Statement.ConnPool if err := assocDB.Where("? IN (?)", primaryColumns, base.Select(ownerFKNames)).Delete(assocModel).Error; err != nil { return err } return base.Updates(fkNil).Error }) case clause.OpUnlink: return base.Updates(fkNil).Error case clause.OpUpdate: return assocDB.Where("? IN (?)", primaryColumns, base.Select(ownerFKNames)).Updates(setMap).Error } case schema.Many2Many: joinModel := reflect.New(rel.JoinTable.ModelType).Interface() joinDB := base.Session(&Session{NewDB: true, Context: ctx}).Model(joinModel) // EXISTS owners: owners.pk = join.owner_fk for all owner refs ownersExists := base.Session(&Session{NewDB: true, Context: ctx}).Table(rel.Schema.Table).Select("1") for _, ref := range rel.References { if ref.OwnPrimaryKey && ref.PrimaryKey != nil { ownersExists = ownersExists.Where(clause.Eq{ Column: clause.Column{Table: rel.Schema.Table, Name: ref.PrimaryKey.DBName}, Value: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, }) } } // EXISTS related: related.pk = join.rel_fk for all related refs, plus optional conditions relatedExists := base.Session(&Session{NewDB: true, Context: ctx}).Table(rel.FieldSchema.Table).Select("1") for _, ref := range rel.References { if !ref.OwnPrimaryKey && ref.PrimaryKey != nil { relatedExists = relatedExists.Where(clause.Eq{ Column: clause.Column{Table: rel.FieldSchema.Table, Name: ref.PrimaryKey.DBName}, Value: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, }) } } relatedExists = relatedExists.Where(op.Conditions) switch op.Type { case clause.OpUnlink, clause.OpDelete: joinDB = joinDB.Where("EXISTS (?)", ownersExists) if len(op.Conditions) > 0 { joinDB = joinDB.Where("EXISTS (?)", relatedExists) } return joinDB.Delete(nil).Error case clause.OpUpdate: // Update related table rows that have join rows matching owners relatedDB := base.Session(&Session{NewDB: true, Context: ctx}).Table(rel.FieldSchema.Table).Where(op.Conditions) // correlated join subquery: join.rel_fk = related.pk AND EXISTS owners joinSub := base.Session(&Session{NewDB: true, Context: ctx}).Table(rel.JoinTable.Table).Select("1") for _, ref := range rel.References { if !ref.OwnPrimaryKey && ref.PrimaryKey != nil { joinSub = joinSub.Where(clause.Eq{ Column: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, Value: clause.Column{Table: rel.FieldSchema.Table, Name: ref.PrimaryKey.DBName}, }) } } joinSub = joinSub.Where("EXISTS (?)", ownersExists) return relatedDB.Where("EXISTS (?)", joinSub).Updates(setMap).Error } } return errors.New("unsupported relationship") }