diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..c3038c737 Binary files /dev/null and b/.DS_Store differ diff --git a/callbacks/delete.go b/callbacks/delete.go index 07ed6feef..133fe922c 100644 --- a/callbacks/delete.go +++ b/callbacks/delete.go @@ -1,6 +1,7 @@ package callbacks import ( + "fmt" "reflect" "strings" @@ -23,9 +24,205 @@ func BeforeDelete(db *gorm.DB) { } } +func parseNestedDelete(schema *schema.Schema, selects []string) map[string][]string { + result := make(map[string][]string) + + for _, selectItem := range selects { + switch { + case selectItem == clause.Associations: + for name := range schema.Relationships.Relations { + result[name] = nil + } + case strings.Contains(selectItem, "."): + parts := strings.Split(selectItem, ".") + if len(parts) > 0 { + firstRel := parts[0] + if _, ok := schema.Relationships.Relations[firstRel]; ok { + if len(parts) > 1 { + result[firstRel] = append(result[firstRel], strings.Join(parts[1:], ".")) + } else { + result[firstRel] = nil + } + } + } + default: + if _, ok := schema.Relationships.Relations[selectItem]; ok { + result[selectItem] = nil + } + } + } + + return result +} + +func deleteNestedAssociations(db *gorm.DB, rel *schema.Relationship, nestedPaths []string) error { + switch rel.Type { + case schema.HasOne, schema.HasMany: + return deleteHasOneOrManyNestedAssociations(db, rel, nestedPaths) + case schema.Many2Many: + return deleteMany2ManyNestedAssociations(db, rel, nestedPaths) + case schema.BelongsTo: + return deleteBelongsToNestedAssociations(db, rel, nestedPaths) + } + return nil +} + +func deleteWithNestedSelect(db *gorm.DB, value interface{}, nestedPaths []string) error { + tx := db.Session(&gorm.Session{NewDB: true}) + for _, path := range nestedPaths { + tx = tx.Select(path) + } + return tx.Delete(value).Error +} + func DeleteBeforeAssociations(db *gorm.DB) { if db.Error == nil && db.Statement.Schema != nil { + if len(db.Statement.Selects) > 0 { + hasRelationshipSelects := false + for _, selectItem := range db.Statement.Selects { + if selectItem == clause.Associations { + hasRelationshipSelects = true + break + } + if _, ok := db.Statement.Schema.Relationships.Relations[selectItem]; ok { + hasRelationshipSelects = true + break + } + if strings.Contains(selectItem, ".") { + parts := strings.Split(selectItem, ".") + if len(parts) > 0 { + if _, ok := db.Statement.Schema.Relationships.Relations[parts[0]]; ok { + hasRelationshipSelects = true + break + } + } + } + } + + if hasRelationshipSelects { + hasClauseAssociations := false + var otherSelects []string + for _, s := range db.Statement.Selects { + if s == clause.Associations { + hasClauseAssociations = true + } else { + otherSelects = append(otherSelects, s) + } + } + + if hasClauseAssociations { + selectColumns, restricted := db.Statement.SelectAndOmitColumns(true, false) + + if restricted { + explicitRelations := make(map[string]bool) + for _, s := range otherSelects { + if strings.Contains(s, ".") { + parts := strings.Split(s, ".") + if len(parts) > 0 { + explicitRelations[parts[0]] = true + } + } else { + explicitRelations[s] = true + } + } + + for column, v := range selectColumns { + if !v { + continue + } + + if explicitRelations[column] { + continue + } + + rel, ok := db.Statement.Schema.Relationships.Relations[column] + if !ok { + continue + } + + if err := deleteAssociation(db, rel); err != nil { + _ = db.AddError(err) + return + } + } + } + } + + if len(otherSelects) > 0 { + for _, selectItem := range otherSelects { + if selectItem != clause.Associations { + parts := strings.Split(selectItem, ".") + if len(parts) > 0 { + firstRel := parts[0] + if _, ok := db.Statement.Schema.Relationships.Relations[firstRel]; !ok { + if field := db.Statement.Schema.LookUpField(firstRel); field != nil { + _ = db.AddError(fmt.Errorf("field %s is not a valid relationship", firstRel)) + return + } else { + _ = db.AddError(fmt.Errorf("%s is not a valid relationship", firstRel)) + return + } + } + } + } + } + + if db.Statement.ReflectValue.Kind() == reflect.Struct || db.Statement.ReflectValue.Kind() == reflect.Slice { + needsLoad := false + + if db.Statement.ReflectValue.Kind() == reflect.Struct { + isZero := true + for _, field := range db.Statement.Schema.PrimaryFields { + value, _ := field.ValueOf(db.Statement.Context, db.Statement.ReflectValue) + if !reflect.ValueOf(value).IsZero() { + isZero = false + break + } + } + needsLoad = isZero + } else if db.Statement.ReflectValue.Kind() == reflect.Slice { + needsLoad = db.Statement.ReflectValue.Len() == 0 + } + + if needsLoad { + loadDB := db.Session(&gorm.Session{NewDB: true}).Model(db.Statement.Dest) + if db.Statement.Unscoped { + loadDB = loadDB.Unscoped() + } + + if whereClause, ok := db.Statement.Clauses["WHERE"]; ok { + if where, ok := whereClause.Expression.(clause.Where); ok { + loadDB.Statement.AddClause(where) + } + } + + if err := loadDB.First(db.Statement.Dest).Error; err != nil { + return + } + db.Statement.ReflectValue = reflect.ValueOf(db.Statement.Dest).Elem() + } + } + + nestedDeletes := parseNestedDelete(db.Statement.Schema, otherSelects) + + for relName, nestedPaths := range nestedDeletes { + rel := db.Statement.Schema.Relationships.Relations[relName] + if rel == nil { + continue + } + + if err := deleteNestedAssociations(db, rel, nestedPaths); err != nil { + _ = db.AddError(err) + return + } + } + } + return + } + } + selectColumns, restricted := db.Statement.SelectAndOmitColumns(true, false) + if !restricted { return } @@ -40,74 +237,77 @@ func DeleteBeforeAssociations(db *gorm.DB) { continue } - switch rel.Type { - case schema.HasOne, schema.HasMany: - queryConds := rel.ToQueryConditions(db.Statement.Context, db.Statement.ReflectValue) - modelValue := reflect.New(rel.FieldSchema.ModelType).Interface() - tx := db.Session(&gorm.Session{NewDB: true}).Model(modelValue) - withoutConditions := false - if db.Statement.Unscoped { - tx = tx.Unscoped() - } + if err := deleteAssociation(db, rel); err != nil { + _ = db.AddError(err) + return + } + } + } +} - if len(db.Statement.Selects) > 0 { - selects := make([]string, 0, len(db.Statement.Selects)) - for _, s := range db.Statement.Selects { - if s == clause.Associations { - selects = append(selects, s) - } else if columnPrefix := column + "."; strings.HasPrefix(s, columnPrefix) { - selects = append(selects, strings.TrimPrefix(s, columnPrefix)) - } - } +func deleteAssociation(db *gorm.DB, rel *schema.Relationship) error { + switch rel.Type { + case schema.HasOne, schema.HasMany: + return deleteHasOneOrManyAssociation(db, rel) + case schema.Many2Many: + return deleteMany2ManyAssociation(db, rel) + case schema.BelongsTo: + // For clause.Associations, BelongsTo should not be deleted + // as it would violate foreign key constraints + return nil + } + return nil +} - if len(selects) > 0 { - tx = tx.Select(selects) - } - } +func deleteHasOneOrManyAssociation(db *gorm.DB, rel *schema.Relationship) error { + queryConds := rel.ToQueryConditions(db.Statement.Context, db.Statement.ReflectValue) + modelValue := reflect.New(rel.FieldSchema.ModelType).Interface() + tx := db.Session(&gorm.Session{NewDB: true}).Model(modelValue) - for _, cond := range queryConds { - if c, ok := cond.(clause.IN); ok && len(c.Values) == 0 { - withoutConditions = true - break - } - } + if db.Statement.Unscoped { + tx = tx.Unscoped() + } - if !withoutConditions && db.AddError(tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue).Error) != nil { - return - } - case schema.Many2Many: - var ( - queryConds = make([]clause.Expression, 0, len(rel.References)) - foreignFields = make([]*schema.Field, 0, len(rel.References)) - relForeignKeys = make([]string, 0, len(rel.References)) - modelValue = reflect.New(rel.JoinTable.ModelType).Interface() - table = rel.JoinTable.Table - tx = db.Session(&gorm.Session{NewDB: true}).Model(modelValue).Table(table) - ) - - for _, ref := range rel.References { - if ref.OwnPrimaryKey { - foreignFields = append(foreignFields, ref.PrimaryKey) - relForeignKeys = append(relForeignKeys, ref.ForeignKey.DBName) - } else if ref.PrimaryValue != "" { - queryConds = append(queryConds, clause.Eq{ - Column: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, - Value: ref.PrimaryValue, - }) - } - } + if hasEmptyConditions(queryConds) { + return nil + } - _, foreignValues := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, foreignFields) - column, values := schema.ToQueryValues(table, relForeignKeys, foreignValues) - queryConds = append(queryConds, clause.IN{Column: column, Values: values}) + return tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue).Error +} - if db.AddError(tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue).Error) != nil { - return - } - } +func deleteMany2ManyAssociation(db *gorm.DB, rel *schema.Relationship) error { + var ( + queryConds = make([]clause.Expression, 0, len(rel.References)) + foreignFields = make([]*schema.Field, 0, len(rel.References)) + relForeignKeys = make([]string, 0, len(rel.References)) + modelValue = reflect.New(rel.JoinTable.ModelType).Interface() + table = rel.JoinTable.Table + tx = db.Session(&gorm.Session{NewDB: true}).Model(modelValue).Table(table) + ) + + for _, ref := range rel.References { + if ref.OwnPrimaryKey { + foreignFields = append(foreignFields, ref.PrimaryKey) + relForeignKeys = append(relForeignKeys, ref.ForeignKey.DBName) + } else if ref.PrimaryValue != "" { + queryConds = append(queryConds, clause.Eq{ + Column: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, + Value: ref.PrimaryValue, + }) } + } + _, foreignValues := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, foreignFields) + column, values := schema.ToQueryValues(table, relForeignKeys, foreignValues) + + if len(values) > 0 { + queryConds = append(queryConds, clause.IN{Column: column, Values: values}) + } + + if len(queryConds) > 0 { + return tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue).Error } + return nil } func Delete(config *Config) func(db *gorm.DB) { @@ -193,3 +393,175 @@ func AfterDelete(db *gorm.DB) { }) } } + +func hasEmptyConditions(queryConds []clause.Expression) bool { + for _, cond := range queryConds { + if c, ok := cond.(clause.IN); ok && len(c.Values) == 0 { + return true + } + } + return false +} + +func deleteHasOneOrManyNestedAssociations(db *gorm.DB, rel *schema.Relationship, nestedPaths []string) error { + queryConds := rel.ToQueryConditions(db.Statement.Context, db.Statement.ReflectValue) + modelValue := reflect.New(rel.FieldSchema.ModelType).Interface() + tx := db.Session(&gorm.Session{NewDB: true}).Model(modelValue) + + if db.Statement.Unscoped { + tx = tx.Unscoped() + } + + if hasEmptyConditions(queryConds) { + return nil + } + + if len(nestedPaths) > 0 { + return deleteWithNestedPaths(tx, rel, queryConds, nestedPaths) + } + + result := tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue) + return result.Error +} + +func deleteWithNestedPaths(tx *gorm.DB, rel *schema.Relationship, queryConds []clause.Expression, nestedPaths []string) error { + var records reflect.Value + searchTx := tx.Unscoped() + + if rel.Type == schema.HasOne { + records = reflect.New(rel.FieldSchema.ModelType) + if err := searchTx.Clauses(clause.Where{Exprs: queryConds}).First(records.Interface()).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return err + } + if records.Elem().IsZero() { + return nil + } + } else { + records = reflect.New(reflect.SliceOf(rel.FieldSchema.ModelType)) + if err := searchTx.Clauses(clause.Where{Exprs: queryConds}).Find(records.Interface()).Error; err != nil { + return err + } + if records.Elem().Len() == 0 { + return nil + } + } + + return deleteWithNestedSelect(tx, records.Interface(), nestedPaths) +} + +func deleteMany2ManyNestedAssociations(db *gorm.DB, rel *schema.Relationship, nestedPaths []string) error { + associatedRecords, err := findMany2ManyAssociatedRecords(db, rel) + if err != nil { + return err + } + + if associatedRecords.IsValid() && associatedRecords.Elem().Len() > 0 { + if len(nestedPaths) > 0 { + if err := deleteWithNestedSelect(db.Session(&gorm.Session{NewDB: true}), associatedRecords.Interface(), nestedPaths); err != nil { + return err + } + } else { + if err := db.Session(&gorm.Session{NewDB: true}).Delete(associatedRecords.Interface()).Error; err != nil { + return err + } + } + } + + return deleteMany2ManyJoinTable(db, rel) +} + +func findMany2ManyAssociatedRecords(db *gorm.DB, rel *schema.Relationship) (reflect.Value, error) { + joinTable := rel.JoinTable.Table + selectQuery := db.Session(&gorm.Session{NewDB: true}) + + if db.Statement.Unscoped { + selectQuery = selectQuery.Unscoped() + } + + var joinConditions []string + var queryArgs []interface{} + + for _, ref := range rel.References { + if ref.OwnPrimaryKey { + if db.Statement.ReflectValue.Kind() == reflect.Slice { + continue + } + value, _ := ref.PrimaryKey.ValueOf(db.Statement.Context, db.Statement.ReflectValue) + joinConditions = append(joinConditions, joinTable+"."+ref.ForeignKey.DBName+" = ?") + queryArgs = append(queryArgs, value) + } + } + + if len(joinConditions) == 0 { + return reflect.Value{}, nil + } + + if len(rel.References) == 0 || len(rel.FieldSchema.PrimaryFieldDBNames) == 0 { + return reflect.Value{}, fmt.Errorf("missing references or primary field names for relationship") + } + + associatedRecords := reflect.New(reflect.SliceOf(rel.FieldSchema.ModelType)) + query := selectQuery.Table(rel.FieldSchema.Table). + Joins("INNER JOIN "+joinTable+" ON "+rel.FieldSchema.Table+"."+rel.FieldSchema.PrimaryFieldDBNames[0]+" = "+joinTable+"."+rel.References[len(rel.References)-1].ForeignKey.DBName). + Where(strings.Join(joinConditions, " AND "), queryArgs...) + + err := query.Find(associatedRecords.Interface()).Error + return associatedRecords, err +} + +func deleteMany2ManyJoinTable(db *gorm.DB, rel *schema.Relationship) error { + var ( + queryConds = make([]clause.Expression, 0, len(rel.References)) + foreignFields = make([]*schema.Field, 0, len(rel.References)) + relForeignKeys = make([]string, 0, len(rel.References)) + modelValue = reflect.New(rel.JoinTable.ModelType).Interface() + table = rel.JoinTable.Table + tx = db.Session(&gorm.Session{NewDB: true}).Model(modelValue).Table(table) + ) + + for _, ref := range rel.References { + if ref.OwnPrimaryKey { + foreignFields = append(foreignFields, ref.PrimaryKey) + relForeignKeys = append(relForeignKeys, ref.ForeignKey.DBName) + } else if ref.PrimaryValue != "" { + queryConds = append(queryConds, clause.Eq{ + Column: clause.Column{Table: rel.JoinTable.Table, Name: ref.ForeignKey.DBName}, + Value: ref.PrimaryValue, + }) + } + } + + _, foreignValues := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, foreignFields) + column, values := schema.ToQueryValues(table, relForeignKeys, foreignValues) + + if len(values) > 0 { + queryConds = append(queryConds, clause.IN{Column: column, Values: values}) + } + + if len(queryConds) > 0 { + return tx.Clauses(clause.Where{Exprs: queryConds}).Delete(modelValue).Error + } + return nil +} + +func deleteBelongsToNestedAssociations(db *gorm.DB, rel *schema.Relationship, nestedPaths []string) error { + if len(nestedPaths) == 0 { + return nil + } + + queryConds := rel.ToQueryConditions(db.Statement.Context, db.Statement.ReflectValue) + modelValue := reflect.New(rel.FieldSchema.ModelType).Interface() + tx := db.Session(&gorm.Session{NewDB: true}).Model(modelValue) + + if err := tx.Clauses(clause.Where{Exprs: queryConds}).First(modelValue).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return err + } + + return deleteWithNestedSelect(tx, modelValue, nestedPaths) +} diff --git a/tests/delete_test.go b/tests/delete_test.go index b9b5289c1..c556f9b68 100644 --- a/tests/delete_test.go +++ b/tests/delete_test.go @@ -256,3 +256,1142 @@ func TestDeleteReturning(t *testing.T) { t.Errorf("failed to delete data, current count %v", count) } } + +func TestNestedDelete(t *testing.T) { + type NestedDeleteProfile struct { + gorm.Model + Name string + NestedDeleteUserID uint + } + + type NestedDeleteUser struct { + gorm.Model + Name string + Profiles []NestedDeleteProfile `gorm:"foreignKey:NestedDeleteUserID"` + } + + DB.Migrator().DropTable(&NestedDeleteProfile{}, &NestedDeleteUser{}) + if err := DB.AutoMigrate(&NestedDeleteUser{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + if err := DB.AutoMigrate(&NestedDeleteProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := NestedDeleteUser{Name: "nested_delete_test", Profiles: []NestedDeleteProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + }} + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + t.Logf("Created user with ID: %d", user.ID) + + var deletedUser NestedDeleteUser + result := DB.Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with nested select, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after nested delete, got %d", count) + } + + DB.Model(&NestedDeleteProfile{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 profiles after nested delete, got %d", count) + } +} + +func TestNestedDeleteWithBelongsTo(t *testing.T) { + type NestedDeleteAuthor struct { + gorm.Model + Name string + } + + type NestedDeleteBook struct { + gorm.Model + Title string + AuthorID uint + Author NestedDeleteAuthor + } + + DB.Migrator().DropTable(&NestedDeleteAuthor{}, &NestedDeleteBook{}) + if err := DB.AutoMigrate(&NestedDeleteAuthor{}, &NestedDeleteBook{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + author := NestedDeleteAuthor{Name: "Test Author"} + DB.Create(&author) + + book := NestedDeleteBook{Title: "Test Book", AuthorID: author.ID} + DB.Create(&book) + + var deletedBook NestedDeleteBook + result := DB.Select("Author").Delete(&deletedBook, book.ID) + if result.Error != nil { + t.Fatalf("Failed to delete book with nested BelongsTo, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteBook{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 books after nested delete with BelongsTo, got %d", count) + } + + DB.Model(&NestedDeleteAuthor{}).Count(&count) + if count != 1 { + t.Fatalf("Expected 1 author after nested delete with BelongsTo, got %d", count) + } +} + +func TestNestedDeleteWithManyToMany(t *testing.T) { + type NestedDeleteTag struct { + gorm.Model + Name string + } + + type NestedDeletePost struct { + gorm.Model + Title string + Tags []NestedDeleteTag `gorm:"many2many:nested_delete_post_tags;"` + } + + DB.Migrator().DropTable(&NestedDeleteTag{}, &NestedDeletePost{}, "nested_delete_post_tags") + if err := DB.AutoMigrate(&NestedDeleteTag{}, &NestedDeletePost{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + tag1 := NestedDeleteTag{Name: "Tag1"} + tag2 := NestedDeleteTag{Name: "Tag2"} + DB.Create(&tag1) + DB.Create(&tag2) + + post := NestedDeletePost{Title: "Test Post", Tags: []NestedDeleteTag{tag1, tag2}} + DB.Create(&post) + + var deletedPost NestedDeletePost + result := DB.Select("Tags").Delete(&deletedPost, post.ID) + if result.Error != nil { + t.Fatalf("Failed to delete post with nested ManyToMany, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeletePost{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 posts after nested delete with ManyToMany, got %d", count) + } + + DB.Model(&NestedDeleteTag{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 tags after nested delete with ManyToMany, got %d", count) + } + + DB.Table("nested_delete_post_tags").Count(&count) + if count != 0 { + t.Fatalf("Expected 0 join table records after nested delete with ManyToMany, got %d", count) + } +} + +func TestNestedDeleteWithEmbeddedStruct(t *testing.T) { + type NestedDeleteAddress struct { + Street string + City string + } + + type NestedDeleteEmbeddedUser struct { + gorm.Model + Name string + Address NestedDeleteAddress `gorm:"embedded"` + } + + DB.Migrator().DropTable(&NestedDeleteEmbeddedUser{}) + if err := DB.AutoMigrate(&NestedDeleteEmbeddedUser{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := NestedDeleteEmbeddedUser{ + Name: "embedded_delete_test", + Address: NestedDeleteAddress{ + Street: "123 Main St", + City: "Test City", + }, + } + + DB.Create(&user) + + var deletedUser NestedDeleteEmbeddedUser + result := DB.Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with embedded struct, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteEmbeddedUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after delete with embedded struct, got %d", count) + } +} + +func TestNestedDeleteDeepNesting(t *testing.T) { + type NestedDeleteDeepComment struct { + gorm.Model + Content string + PostID uint + } + + type NestedDeleteDeepNestingPost struct { + gorm.Model + Title string + UserID uint + Comments []NestedDeleteDeepComment `gorm:"foreignKey:PostID"` + } + + type NestedDeleteDeepNestingUser struct { + gorm.Model + Name string + Posts []NestedDeleteDeepNestingPost `gorm:"foreignKey:UserID"` + } + + DB.Migrator().DropTable(&NestedDeleteDeepComment{}, &NestedDeleteDeepNestingPost{}, &NestedDeleteDeepNestingUser{}) + if err := DB.AutoMigrate(&NestedDeleteDeepNestingUser{}, &NestedDeleteDeepNestingPost{}, &NestedDeleteDeepComment{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := NestedDeleteDeepNestingUser{Name: "deep_nesting_test", Posts: []NestedDeleteDeepNestingPost{ + {Title: "Post1", Comments: []NestedDeleteDeepComment{ + {Content: "Comment1"}, + {Content: "Comment2"}, + }}, + {Title: "Post2", Comments: []NestedDeleteDeepComment{ + {Content: "Comment3"}, + }}, + }} + DB.Create(&user) + + var deletedUser NestedDeleteDeepNestingUser + result := DB.Select("Posts.Comments").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with deep nesting, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteDeepNestingUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after deep nested delete, got %d", count) + } + DB.Model(&NestedDeleteDeepNestingPost{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 posts after deep nested delete, got %d", count) + } + DB.Model(&NestedDeleteDeepComment{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 comments after deep nested delete, got %d", count) + } +} + +func TestNestedDeleteMultipleRelations(t *testing.T) { + type NestedDeleteMultiProfile struct { + gorm.Model + Name string + MultiUserID uint + } + + type NestedDeleteMultiPost struct { + gorm.Model + Title string + MultiUserID uint + } + + type NestedDeleteMultiUser struct { + gorm.Model + Name string + Profiles []NestedDeleteMultiProfile `gorm:"foreignKey:MultiUserID"` + Posts []NestedDeleteMultiPost `gorm:"foreignKey:MultiUserID"` + } + + DB.Migrator().DropTable(&NestedDeleteMultiProfile{}, &NestedDeleteMultiPost{}, &NestedDeleteMultiUser{}) + if err := DB.AutoMigrate(&NestedDeleteMultiUser{}, &NestedDeleteMultiPost{}, &NestedDeleteMultiProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user1 := NestedDeleteMultiUser{Name: "multi_relation_test1", Profiles: []NestedDeleteMultiProfile{{Name: "Profile1"}}} + DB.Create(&user1) + + var deletedUser1 NestedDeleteMultiUser + result := DB.Select("Profiles").Delete(&deletedUser1, user1.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with Profiles relation, got error %v", result.Error) + } + + user2 := NestedDeleteMultiUser{Name: "multi_relation_test2", Posts: []NestedDeleteMultiPost{{Title: "Post1"}}} + DB.Create(&user2) + + var deletedUser2 NestedDeleteMultiUser + result = DB.Select("Posts").Delete(&deletedUser2, user2.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with Posts relation, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteMultiUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after multi-relation delete, got %d", count) + } + DB.Model(&NestedDeleteMultiPost{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 posts after multi-relation delete, got %d", count) + } + DB.Model(&NestedDeleteMultiProfile{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 profiles after multi-relation delete, got %d", count) + } +} + +func TestNestedDeleteWithPolymorphic(t *testing.T) { + type NestedDeleteToy struct { + gorm.Model + Name string + OwnerID uint + OwnerType string + } + + type NestedDeleteCat struct { + gorm.Model + Name string + Toys []NestedDeleteToy `gorm:"polymorphic:Owner;"` + } + + DB.Migrator().DropTable(&NestedDeleteToy{}, &NestedDeleteCat{}) + if err := DB.AutoMigrate(&NestedDeleteCat{}, &NestedDeleteToy{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + cat := NestedDeleteCat{Name: "Fluffy", Toys: []NestedDeleteToy{{Name: "Ball"}, {Name: "Mouse"}}} + DB.Create(&cat) + + var deletedCat NestedDeleteCat + result := DB.Select("Toys").Delete(&deletedCat, cat.ID) + if result.Error != nil { + t.Fatalf("Failed to delete cat with polymorphic toys, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteCat{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 cats after polymorphic nested delete, got %d", count) + } + DB.Model(&NestedDeleteToy{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 toys after polymorphic nested delete, got %d", count) + } +} + +func TestNestedDeleteWithSelfReferential(t *testing.T) { + type NestedDeleteCategory struct { + gorm.Model + Name string + ParentID *uint + Parent *NestedDeleteCategory + Children []NestedDeleteCategory `gorm:"foreignKey:ParentID"` + } + + DB.Migrator().DropTable(&NestedDeleteCategory{}) + if err := DB.AutoMigrate(&NestedDeleteCategory{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + parent := NestedDeleteCategory{Name: "Parent"} + DB.Create(&parent) + + child1 := NestedDeleteCategory{Name: "Child1", ParentID: &parent.ID} + child2 := NestedDeleteCategory{Name: "Child2", ParentID: &parent.ID} + DB.Create(&child1) + DB.Create(&child2) + + var deletedParent NestedDeleteCategory + result := DB.Select("Children").Delete(&deletedParent, parent.ID) + if result.Error != nil { + t.Fatalf("Failed to delete parent with children, got error %v", result.Error) + } + + var count int64 + DB.Model(&NestedDeleteCategory{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 categories after self-referential nested delete, got %d", count) + } +} + +// TestNestedDeleteWithEmptyAssociations tests deletion when associations are empty +func TestNestedDeleteWithEmptyAssociations(t *testing.T) { + type EmptyAssocProfile struct { + gorm.Model + Name string + EmptyAssocUserID uint + } + + type EmptyAssocUser struct { + gorm.Model + Name string + Profiles []EmptyAssocProfile `gorm:"foreignKey:EmptyAssocUserID"` + } + + DB.Migrator().DropTable(&EmptyAssocProfile{}, &EmptyAssocUser{}) + if err := DB.AutoMigrate(&EmptyAssocUser{}, &EmptyAssocProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create user without any profiles + user := EmptyAssocUser{Name: "empty_assoc_user"} + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Delete with nested select when no associations exist + var deletedUser EmptyAssocUser + result := DB.Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with empty nested select, got error %v", result.Error) + } + + var count int64 + DB.Model(&EmptyAssocUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after delete with empty nested select, got %d", count) + } +} + +// TestNestedDeleteWithNullableAssociation tests deletion with nullable foreign keys +func TestNestedDeleteWithNullableAssociation(t *testing.T) { + type NullableAssocCompany struct { + gorm.Model + Name string + } + + type NullableAssocUser struct { + gorm.Model + Name string + CompanyID *uint + Company *NullableAssocCompany + } + + DB.Migrator().DropTable(&NullableAssocUser{}, &NullableAssocCompany{}) + if err := DB.AutoMigrate(&NullableAssocCompany{}, &NullableAssocUser{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create user without company + user := NullableAssocUser{Name: "nullable_user", CompanyID: nil} + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Delete with nested select when association is null + var deletedUser NullableAssocUser + result := DB.Select("Company").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with null nested select, got error %v", result.Error) + } + + var count int64 + DB.Model(&NullableAssocUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after delete with null association, got %d", count) + } +} + +// TestNestedDeleteWithSliceOfPointers tests deletion with slice of pointers +func TestNestedDeleteWithSliceOfPointers(t *testing.T) { + type SlicePtrProfile struct { + gorm.Model + Name string + SlicePtrUserID uint + } + + type SlicePtrUser struct { + gorm.Model + Name string + Profiles []*SlicePtrProfile `gorm:"foreignKey:SlicePtrUserID"` + } + + DB.Migrator().DropTable(&SlicePtrProfile{}, &SlicePtrUser{}) + if err := DB.AutoMigrate(&SlicePtrUser{}, &SlicePtrProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := SlicePtrUser{ + Name: "slice_ptr_user", + Profiles: []*SlicePtrProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + {Name: "Profile3"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user with pointer profiles, got error %v", err) + } + + var deletedUser SlicePtrUser + result := DB.Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with slice of pointers, got error %v", result.Error) + } + + var count int64 + DB.Model(&SlicePtrProfile{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 profiles after nested delete with slice of pointers, got %d", count) + } +} + +// TestNestedDeleteHasOneZeroStruct tests HasOne deletion with zero struct +func TestNestedDeleteHasOneZeroStruct(t *testing.T) { + type HasOneZeroAccount struct { + gorm.Model + Name string + HasOneZeroUserID uint + } + + type HasOneZeroUser struct { + gorm.Model + Name string + Account *HasOneZeroAccount `gorm:"foreignKey:HasOneZeroUserID"` + } + + DB.Migrator().DropTable(&HasOneZeroAccount{}, &HasOneZeroUser{}) + if err := DB.AutoMigrate(&HasOneZeroUser{}, &HasOneZeroAccount{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create user without account + user := HasOneZeroUser{Name: "has_one_zero_user"} + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Delete with HasOne nested select when association doesn't exist + var deletedUser HasOneZeroUser + result := DB.Select("Account").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with zero HasOne association, got error %v", result.Error) + } + + var count int64 + DB.Model(&HasOneZeroUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after HasOne zero delete, got %d", count) + } +} + +// TestNestedDeleteWithManyToManyEmptyJoinTable tests M2M deletion with no join table records +func TestNestedDeleteWithManyToManyEmptyJoinTable(t *testing.T) { + type EmptyM2MTag struct { + gorm.Model + Name string + } + + type EmptyM2MPost struct { + gorm.Model + Title string + Tags []EmptyM2MTag `gorm:"many2many:empty_m2m_post_tags;"` + } + + DB.Migrator().DropTable(&EmptyM2MTag{}, &EmptyM2MPost{}, "empty_m2m_post_tags") + if err := DB.AutoMigrate(&EmptyM2MTag{}, &EmptyM2MPost{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create post without tags + post := EmptyM2MPost{Title: "empty_m2m_post"} + if err := DB.Create(&post).Error; err != nil { + t.Fatalf("Failed to create post, got error %v", err) + } + + // Delete with M2M nested select when no join table records exist + var deletedPost EmptyM2MPost + result := DB.Select("Tags").Delete(&deletedPost, post.ID) + if result.Error != nil { + t.Fatalf("Failed to delete post with empty M2M, got error %v", result.Error) + } + + var count int64 + DB.Model(&EmptyM2MPost{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 posts after empty M2M delete, got %d", count) + } +} + +// TestNestedDeleteMultipleLevelsWithAssociations tests deeply nested deletions with multiple relationship types +func TestNestedDeleteMultipleLevelsWithAssociations(t *testing.T) { + type Level3Item struct { + gorm.Model + Name string + Level2ContainerID uint + } + + type Level2Container struct { + gorm.Model + Name string + Level1ParentID uint + Items []Level3Item `gorm:"foreignKey:Level2ContainerID"` + } + + type Level1Parent struct { + gorm.Model + Name string + Containers []Level2Container `gorm:"foreignKey:Level1ParentID"` + } + + DB.Migrator().DropTable(&Level3Item{}, &Level2Container{}, &Level1Parent{}) + if err := DB.AutoMigrate(&Level1Parent{}, &Level2Container{}, &Level3Item{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + parent := Level1Parent{ + Name: "parent", + Containers: []Level2Container{ + { + Name: "container1", + Items: []Level3Item{ + {Name: "item1"}, + {Name: "item2"}, + }, + }, + { + Name: "container2", + Items: []Level3Item{ + {Name: "item3"}, + }, + }, + }, + } + if err := DB.Create(&parent).Error; err != nil { + t.Fatalf("Failed to create nested structure, got error %v", err) + } + + // Delete parent with nested items + var deletedParent Level1Parent + result := DB.Select("Containers.Items").Delete(&deletedParent, parent.ID) + if result.Error != nil { + t.Fatalf("Failed to delete parent with deeply nested items, got error %v", result.Error) + } + + var countParent, countContainer, countItem int64 + DB.Model(&Level1Parent{}).Count(&countParent) + DB.Model(&Level2Container{}).Count(&countContainer) + DB.Model(&Level3Item{}).Count(&countItem) + + if countParent != 0 { + t.Fatalf("Expected 0 parents after nested delete, got %d", countParent) + } + if countContainer != 0 { + t.Fatalf("Expected 0 containers after nested delete, got %d", countContainer) + } + if countItem != 0 { + t.Fatalf("Expected 0 items after nested delete, got %d", countItem) + } +} + +// TestNestedDeleteWithPartialNestedSelection tests deleting only specific nested associations +func TestNestedDeleteWithPartialNestedSelection(t *testing.T) { + type PartialSelProfile struct { + gorm.Model + Name string + PartialSelUserID uint + } + + type PartialSelPost struct { + gorm.Model + Title string + PartialSelUserID uint + } + + type PartialSelUser struct { + gorm.Model + Name string + Profiles []PartialSelProfile `gorm:"foreignKey:PartialSelUserID"` + Posts []PartialSelPost `gorm:"foreignKey:PartialSelUserID"` + } + + DB.Migrator().DropTable(&PartialSelProfile{}, &PartialSelPost{}, &PartialSelUser{}) + if err := DB.AutoMigrate(&PartialSelUser{}, &PartialSelProfile{}, &PartialSelPost{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := PartialSelUser{ + Name: "partial_sel_user", + Profiles: []PartialSelProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + }, + Posts: []PartialSelPost{ + {Title: "Post1"}, + {Title: "Post2"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Delete only profiles nested association + var deletedUser PartialSelUser + result := DB.Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete user with partial nested select, got error %v", result.Error) + } + + var countUser, countProfile, countPost int64 + DB.Model(&PartialSelUser{}).Count(&countUser) + DB.Model(&PartialSelProfile{}).Count(&countProfile) + DB.Model(&PartialSelPost{}).Count(&countPost) + + // User and profiles should be deleted + if countUser != 0 { + t.Fatalf("Expected 0 users after partial delete, got %d", countUser) + } + if countProfile != 0 { + t.Fatalf("Expected 0 profiles after partial delete, got %d", countProfile) + } + // Posts remain because they were not selected for deletion + if countPost != 2 { + t.Fatalf("Expected 2 posts after partial delete (not selected), got %d", countPost) + } +} + +// TestNestedDeleteWithPreloadedData tests deletion with preloaded associations +func TestNestedDeleteWithPreloadedData(t *testing.T) { + type PreloadProfile struct { + gorm.Model + Name string + PreloadUserID uint + } + + type PreloadUser struct { + gorm.Model + Name string + Profiles []PreloadProfile `gorm:"foreignKey:PreloadUserID"` + } + + DB.Migrator().DropTable(&PreloadProfile{}, &PreloadUser{}) + if err := DB.AutoMigrate(&PreloadUser{}, &PreloadProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := PreloadUser{ + Name: "preload_user", + Profiles: []PreloadProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Load user with preloaded associations + var loadedUser PreloadUser + if err := DB.Preload("Profiles").First(&loadedUser, user.ID).Error; err != nil { + t.Fatalf("Failed to preload user, got error %v", err) + } + + // Delete using preloaded data + result := DB.Select("Profiles").Delete(&loadedUser) + if result.Error != nil { + t.Fatalf("Failed to delete user with preloaded data, got error %v", result.Error) + } + + var count int64 + DB.Model(&PreloadUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after delete with preloaded data, got %d", count) + } +} + +// TestNestedDeleteWithSoftDelete tests nested deletion with soft delete enabled +func TestNestedDeleteWithSoftDeleteNested(t *testing.T) { + type SoftDelProfile struct { + gorm.Model + Name string + SoftDelUserID uint + } + + type SoftDelUser struct { + gorm.Model + Name string + Profiles []SoftDelProfile `gorm:"foreignKey:SoftDelUserID"` + } + + DB.Migrator().DropTable(&SoftDelProfile{}, &SoftDelUser{}) + if err := DB.AutoMigrate(&SoftDelUser{}, &SoftDelProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := SoftDelUser{ + Name: "soft_del_user", + Profiles: []SoftDelProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Soft delete with nested select + var deletedUser SoftDelUser + result := DB.Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to soft delete with nested select, got error %v", result.Error) + } + + // Check soft deleted records are still there + var countScoped, countUnscoped int64 + DB.Model(&SoftDelUser{}).Count(&countScoped) + DB.Model(&SoftDelUser{}).Unscoped().Count(&countUnscoped) + + if countScoped != 0 { + t.Fatalf("Expected 0 undeleted users after soft delete, got %d", countScoped) + } + if countUnscoped != 1 { + t.Fatalf("Expected 1 soft-deleted user when using Unscoped, got %d", countUnscoped) + } +} + +// TestNestedDeleteWithUnscopedNested tests hard deletion of nested associations +func TestNestedDeleteWithUnscopedNested(t *testing.T) { + type UnscopedProfile struct { + gorm.Model + Name string + UnscopedUserID uint + } + + type UnscopedUser struct { + gorm.Model + Name string + Profiles []UnscopedProfile `gorm:"foreignKey:UnscopedUserID"` + } + + DB.Migrator().DropTable(&UnscopedProfile{}, &UnscopedUser{}) + if err := DB.AutoMigrate(&UnscopedUser{}, &UnscopedProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := UnscopedUser{ + Name: "unscoped_user", + Profiles: []UnscopedProfile{ + {Name: "Profile1"}, + {Name: "Profile2"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Hard delete with nested select using Unscoped + var deletedUser UnscopedUser + result := DB.Unscoped().Select("Profiles").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to unscoped delete with nested select, got error %v", result.Error) + } + + // Check records are completely gone + var countUnscoped int64 + DB.Model(&UnscopedUser{}).Unscoped().Count(&countUnscoped) + + if countUnscoped != 0 { + t.Fatalf("Expected 0 users after unscoped hard delete, got %d", countUnscoped) + } +} + +// TestNestedDeleteWithComplexM2M tests M2M deletion with multiple posts and tags +func TestNestedDeleteWithComplexM2M(t *testing.T) { + type ComplexTag struct { + gorm.Model + Name string + } + + type ComplexPost struct { + gorm.Model + Title string + ComplexBlogID uint + Tags []ComplexTag `gorm:"many2many:complex_m2m_post_tags;"` + } + + type ComplexBlog struct { + gorm.Model + Name string + Posts []ComplexPost `gorm:"foreignKey:ComplexBlogID"` + } + + DB.Migrator().DropTable(&ComplexTag{}, &ComplexPost{}, &ComplexBlog{}, "complex_m2m_post_tags") + if err := DB.AutoMigrate(&ComplexBlog{}, &ComplexPost{}, &ComplexTag{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create tags + tags := []ComplexTag{ + {Name: "golang"}, + {Name: "database"}, + {Name: "testing"}, + } + if err := DB.Create(&tags).Error; err != nil { + t.Fatalf("Failed to create tags, got error %v", err) + } + + // Create blog with posts and tags + blog := ComplexBlog{ + Name: "complex_blog", + Posts: []ComplexPost{ + {Title: "Post1", Tags: []ComplexTag{tags[0], tags[1]}}, + {Title: "Post2", Tags: []ComplexTag{tags[1], tags[2]}}, + {Title: "Post3", Tags: []ComplexTag{tags[0], tags[2]}}, + }, + } + if err := DB.Create(&blog).Error; err != nil { + t.Fatalf("Failed to create blog with posts, got error %v", err) + } + + // Delete blog with posts + var deletedBlog ComplexBlog + result := DB.Select("Posts.Tags").Delete(&deletedBlog, blog.ID) + if result.Error != nil { + t.Fatalf("Failed to delete blog with complex M2M, got error %v", result.Error) + } + + var countBlog, countPost, countTag, countJoin int64 + DB.Model(&ComplexBlog{}).Count(&countBlog) + DB.Model(&ComplexPost{}).Count(&countPost) + DB.Model(&ComplexTag{}).Count(&countTag) + DB.Table("complex_m2m_post_tags").Count(&countJoin) + + if countBlog != 0 { + t.Fatalf("Expected 0 blogs after complex M2M delete, got %d", countBlog) + } + if countPost != 0 { + t.Fatalf("Expected 0 posts after complex M2M delete, got %d", countPost) + } + if countTag != 3 { + t.Fatalf("Expected 3 tags after complex M2M delete (tags should not be deleted), got %d", countTag) + } + if countJoin != 0 { + t.Fatalf("Expected 0 join table records after complex M2M delete, got %d", countJoin) + } +} + +// TestNestedDeleteAssociationsClause tests deletion with clause.Associations +func TestNestedDeleteAssociationsClause(t *testing.T) { + type ClauseProfile struct { + gorm.Model + Name string + ClauseUserID uint + } + + type ClausePost struct { + gorm.Model + Title string + ClauseUserID uint + } + + type ClauseUser struct { + gorm.Model + Name string + Profiles []ClauseProfile `gorm:"foreignKey:ClauseUserID"` + Posts []ClausePost `gorm:"foreignKey:ClauseUserID"` + } + + DB.Migrator().DropTable(&ClauseProfile{}, &ClausePost{}, &ClauseUser{}) + if err := DB.AutoMigrate(&ClauseUser{}, &ClauseProfile{}, &ClausePost{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := ClauseUser{ + Name: "clause_user", + Profiles: []ClauseProfile{ + {Name: "Profile1"}, + }, + Posts: []ClausePost{ + {Title: "Post1"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Delete all associations using clause.Associations + var deletedUser ClauseUser + result := DB.Select(clause.Associations).Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete with clause.Associations, got error %v", result.Error) + } + + var count int64 + DB.Model(&ClauseUser{}).Count(&count) + if count != 0 { + t.Fatalf("Expected 0 users after delete with clause.Associations, got %d", count) + } +} + +// TestNestedDeleteWithPreloadedPartialData tests deletion with preloaded partial data +func TestNestedDeleteWithPreloadedPartialData(t *testing.T) { + type PreloadPartialProfile struct { + gorm.Model + Name string + Description string + PreloadPartialUserID uint + } + + type PreloadPartialUser struct { + gorm.Model + Name string + Email string + Profiles []PreloadPartialProfile `gorm:"foreignKey:PreloadPartialUserID"` + } + + DB.Migrator().DropTable(&PreloadPartialProfile{}, &PreloadPartialUser{}) + if err := DB.AutoMigrate(&PreloadPartialUser{}, &PreloadPartialProfile{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + user := PreloadPartialUser{ + Name: "preload_partial_user", + Email: "user@example.com", + Profiles: []PreloadPartialProfile{ + {Name: "Profile1", Description: "Desc1"}, + {Name: "Profile2", Description: "Desc2"}, + }, + } + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create user, got error %v", err) + } + + // Load user with preloaded profiles but only specific columns (Name, not Description) + var loadedUser PreloadPartialUser + if err := DB.Preload("Profiles", func(db *gorm.DB) *gorm.DB { + return db.Select("id", "name", "preload_partial_user_id") + }).First(&loadedUser, user.ID).Error; err != nil { + t.Fatalf("Failed to preload user with partial columns, got error %v", err) + } + + // Verify that Description field is not loaded (zero value) + if loadedUser.Profiles[0].Description != "" { + // If it's loaded from DB, the description will be there; if truly partial, it won't be + t.Logf("Note: Profiles preloaded with Description field: %s", loadedUser.Profiles[0].Description) + } + + // Delete using the preloaded user (even though it's partial) + result := DB.Select("Profiles").Delete(&loadedUser) + if result.Error != nil { + t.Fatalf("Failed to delete with preloaded partial data, got error %v", result.Error) + } + + var countUser, countProfile int64 + DB.Model(&PreloadPartialUser{}).Count(&countUser) + DB.Model(&PreloadPartialProfile{}).Count(&countProfile) + + if countUser != 0 { + t.Fatalf("Expected 0 users after delete with preloaded partial data, got %d", countUser) + } + if countProfile != 0 { + t.Fatalf("Expected 0 profiles after delete with preloaded partial data, got %d", countProfile) + } +} + +// TestNestedDeleteWithComposedAssociations tests deletion with multiple levels and different relationship types +func TestNestedDeleteWithComposedAssociations(t *testing.T) { + type ComposedToy struct { + gorm.Model + Name string + ComposedProfileID uint + } + + type ComposedProfile struct { + gorm.Model + Name string + ComposedUserID uint + Toys []ComposedToy `gorm:"foreignKey:ComposedProfileID"` + } + + type ComposedUser struct { + gorm.Model + Name string + Profiles []ComposedProfile `gorm:"foreignKey:ComposedUserID"` + } + + DB.Migrator().DropTable(&ComposedToy{}, &ComposedProfile{}, &ComposedUser{}) + if err := DB.AutoMigrate(&ComposedUser{}, &ComposedProfile{}, &ComposedToy{}); err != nil { + t.Fatalf("Failed to auto migrate, got error %v", err) + } + + // Create complex nested structure + user := ComposedUser{ + Name: "composed_user", + Profiles: []ComposedProfile{ + { + Name: "Profile1", + Toys: []ComposedToy{ + {Name: "Toy1A"}, + {Name: "Toy1B"}, + {Name: "Toy1C"}, + }, + }, + { + Name: "Profile2", + Toys: []ComposedToy{ + {Name: "Toy2A"}, + {Name: "Toy2B"}, + }, + }, + { + Name: "Profile3", + Toys: []ComposedToy{ + {Name: "Toy3A"}, + }, + }, + }, + } + + if err := DB.Create(&user).Error; err != nil { + t.Fatalf("Failed to create composed user, got error %v", err) + } + + // Verify structure was created correctly + var countUser, countProfile, countToy int64 + DB.Model(&ComposedUser{}).Count(&countUser) + DB.Model(&ComposedProfile{}).Count(&countProfile) + DB.Model(&ComposedToy{}).Count(&countToy) + + if countUser != 1 { + t.Fatalf("Expected 1 user before delete, got %d", countUser) + } + if countProfile != 3 { + t.Fatalf("Expected 3 profiles before delete, got %d", countProfile) + } + if countToy != 6 { + t.Fatalf("Expected 6 toys before delete, got %d", countToy) + } + + // Delete user with composed associations (user -> profiles -> toys) + var deletedUser ComposedUser + result := DB.Select("Profiles.Toys").Delete(&deletedUser, user.ID) + if result.Error != nil { + t.Fatalf("Failed to delete with composed associations, got error %v", result.Error) + } + + // After delete, verify all nested records are gone + DB.Model(&ComposedUser{}).Count(&countUser) + DB.Model(&ComposedProfile{}).Count(&countProfile) + DB.Model(&ComposedToy{}).Count(&countToy) + + if countUser != 0 { + t.Fatalf("Expected 0 users after composed delete, got %d", countUser) + } + if countProfile != 0 { + t.Fatalf("Expected 0 profiles after composed delete, got %d", countProfile) + } + if countToy != 0 { + t.Fatalf("Expected 0 toys after composed delete, got %d", countToy) + } +}