Skip to content

Conversation

@mic3b
Copy link

@mic3b mic3b commented Sep 28, 2025

  • Do only one thing
  • Non breaking API changes
  • Tested

What did this pull request do?

close #6687

User Case Description

- Introduced `parseNestedDelete` and `deleteNestedAssociations` functions to handle nested deletions based on specified relationships.
- Enhanced `DeleteBeforeAssociations` to support nested deletes when associations are included in the select statement.
- Updated `deleteAssociation` to manage deletion logic for various relationship types, including HasOne, HasMany, Many2Many, and BelongsTo.
- Added comprehensive tests for nested delete scenarios, covering various relationship types and error handling.

This update improves the flexibility and robustness of the delete operations in GORM, allowing for more complex data structures to be managed effectively.
@propel-code-bot
Copy link
Contributor

propel-code-bot bot commented Sep 28, 2025

Add Nested Delete Support for Associations in GORM

This pull request introduces comprehensive support for nested delete operations on associations in GORM. It enables developers to delete not only the primary records but also specified associated relationships (including deeply nested and multiple relationship types such as HasOne, HasMany, Many2Many, BelongsTo, polymorphic, embedded, and self-referential associations) by using the Select() statement. The implementation features robust handling of nested relationships, new helper methods for parsing and processing association paths, detailed error checking, and significant expansion of test coverage for the new nested delete capabilities.

Key Changes

• Added new functions: parseNestedDelete, deleteNestedAssociations, deleteHasOneOrManyNestedAssociations, deleteWithNestedSelect, and supporting helpers to implement recursive, nested association deletion.
• Major refactor of DeleteBeforeAssociations in callbacks/delete.go to recognize and process nested association paths in the Selects list.
• Split original association deletion logic into type-specific functions: deleteHasOneOrManyAssociation, deleteMany2ManyAssociation, and deleteBelongsToNestedAssociations.
• Handling for Many2Many join-table cleanup and robust logic for nested deletes through new helpers like findMany2ManyAssociatedRecords and deleteMany2ManyJoinTable.
• Substantial extension of the test suite in tests/delete_test.go to verify nested deletes for a variety of association types and edge cases, including deep nesting, polymorphic associations, embedded structs, self-referential models, and combinations of these.
• Error handling improvements for invalid relationship paths and database query failures.
• Fixes for all reported golangci-lint errors and CI/CD compatibility.

Affected Areas

callbacks/delete.go: core callbacks and helper functions for delete operations
tests/delete_test.go: delete association (incl. nested) test coverage

This summary was automatically generated by @propel-code-bot

}
}

if err := loadDB.First(db.Statement.Dest).Error; err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CriticalError]

Missing error handling: The database query on line 342 silently ignores errors, which can lead to incomplete deletion operations. The error should be handled properly:

Suggested change
if err := loadDB.First(db.Statement.Dest).Error; err != nil {
if err := loadDB.First(db.Statement.Dest).Error; err != nil {
db.AddError(err)
return
}

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Context for Agents
[**CriticalError**]

Missing error handling: The database query on line 342 silently ignores errors, which can lead to incomplete deletion operations. The error should be handled properly:

```suggestion
if err := loadDB.First(db.Statement.Dest).Error; err != nil {
	db.AddError(err)
	return
}
```

⚡ **Committable suggestion**

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

File: callbacks/delete.go
Line: 342

@propel-code-bot propel-code-bot bot changed the title Add nested delete Add nested delete support for associations in GORM Sep 28, 2025
Comment on lines +451 to +453
if records.Elem().Len() == 0 {
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CriticalError]

Potential runtime panic in deleteWithNestedPaths function: On line 451, records.Elem().Len() == 0 assumes that records contains a slice type, but for schema.HasOne relationships (line 436-443), records is set to reflect.New(rel.FieldSchema.ModelType) which creates a pointer to a single struct, not a slice. Calling .Len() on a struct will panic.

Suggested Change
Suggested change
if records.Elem().Len() == 0 {
return nil
}
if rel.Type == schema.HasOne {
if records.Elem().IsZero() {
return nil
}
} else {
if records.Elem().Len() == 0 {
return nil
}
}

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Context for Agents
[**CriticalError**]

Potential runtime panic in `deleteWithNestedPaths` function: On line 451, `records.Elem().Len() == 0` assumes that `records` contains a slice type, but for `schema.HasOne` relationships (line 436-443), `records` is set to `reflect.New(rel.FieldSchema.ModelType)` which creates a pointer to a single struct, not a slice. Calling `.Len()` on a struct will panic.

<details>
<summary>Suggested Change</summary>

```suggestion
if rel.Type == schema.HasOne {
	if records.Elem().IsZero() {
		return nil
	}
} else {
	if records.Elem().Len() == 0 {
		return nil
	}
}
```

⚡ **Committable suggestion**

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

</details>

File: callbacks/delete.go
Line: 453

Comment on lines +505 to +508
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...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CriticalError]

Array bounds access without validation: Line 507 accesses rel.References[len(rel.References)-1] without checking if rel.References is empty. If rel.References has length 0, this will panic with an index out of bounds error.

Suggested Change
Suggested change
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...)
if len(rel.References) == 0 {
return reflect.Value{}, fmt.Errorf("no references found 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...)

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Context for Agents
[**CriticalError**]

Array bounds access without validation: Line 507 accesses `rel.References[len(rel.References)-1]` without checking if `rel.References` is empty. If `rel.References` has length 0, this will panic with an index out of bounds error.

<details>
<summary>Suggested Change</summary>

```suggestion
if len(rel.References) == 0 {
	return reflect.Value{}, fmt.Errorf("no references found 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...)
```

⚡ **Committable suggestion**

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

</details>

File: callbacks/delete.go
Line: 508

Comment on lines +505 to +508
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...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CriticalError]

Array bounds access without validation: Line 507 also accesses rel.FieldSchema.PrimaryFieldDBNames[0] without checking if the slice is empty. If PrimaryFieldDBNames is empty, this will panic with an index out of bounds error.

Suggested Change
Suggested change
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...)
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...)

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Context for Agents
[**CriticalError**]

Array bounds access without validation: Line 507 also accesses `rel.FieldSchema.PrimaryFieldDBNames[0]` without checking if the slice is empty. If `PrimaryFieldDBNames` is empty, this will panic with an index out of bounds error.

<details>
<summary>Suggested Change</summary>

```suggestion
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...)
```

⚡ **Committable suggestion**

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

</details>

File: callbacks/delete.go
Line: 508

@propel-code-bot propel-code-bot bot changed the title Add nested delete support for associations in GORM Add nested delete functionality for associations in GORM Sep 28, 2025
@propel-code-bot propel-code-bot bot changed the title Add nested delete functionality for associations in GORM Add nested delete support for associations in GORM Sep 28, 2025
@jinzhu jinzhu requested a review from Copilot October 30, 2025 12:03
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for nested delete operations in GORM, allowing cascading deletion of associated records when using Select() with relationship names during delete operations.

Key changes:

  • Added logic to parse and handle nested relationship deletion via Select() clause (e.g., Select("Posts.Comments"))
  • Refactored the delete association logic to support HasOne, HasMany, BelongsTo, and ManyToMany relationships
  • Added comprehensive test coverage for nested deletion scenarios including deep nesting, polymorphic relations, and self-referential structures

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
callbacks/delete.go Implements nested delete functionality with new helper functions for parsing nested selects and deleting associations based on relationship types
tests/delete_test.go Adds 8 new test functions covering various nested delete scenarios including HasMany, BelongsTo, ManyToMany, embedded structs, deep nesting, multiple relations, polymorphic, and self-referential relationships

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +446 to +447
if records.Elem().Len() == 0 {
return nil
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential panic when calling Len() on a non-slice/array reflect.Value. For HasOne relationships, records is a pointer to a single struct (line 432), not a slice, so Elem().Len() will panic. Check if the value is actually a slice before calling Len(), or handle HasOne separately.

Suggested change
if records.Elem().Len() == 0 {
return nil
if rel.Type == schema.HasOne {
// For HasOne, check if the struct is zero (not found)
if records.Elem().IsZero() {
return nil
}
} else {
// For HasMany, check if the slice is empty
if records.Elem().Len() == 0 {
return nil
}

Copilot uses AI. Check for mistakes.
return err
}

if associatedRecords.Elem().Len() > 0 {
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential panic if findMany2ManyAssociatedRecords returns an empty reflect.Value{} (line 497). Before calling Elem().Len(), check if associatedRecords.IsValid() to avoid panicking on an invalid reflect.Value.

Suggested change
if associatedRecords.Elem().Len() > 0 {
if associatedRecords.IsValid() && associatedRecords.Elem().Len() > 0 {

Copilot uses AI. Check for mistakes.
DB.Create(&user)

var deletedUser NestedDeleteDeepNestingUser
result := DB.Select("Posts.Comments").Delete(&deletedUser, user.ID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is the only test for nested associations, there must many cases are not covered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: support nested delete

2 participants