-
Notifications
You must be signed in to change notification settings - Fork 248
feat: Add dependent: :adopt option for has_closure_tree
#471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
@seuros |
dependent: :adopt option for has_closure_treedependent: :adopt option for has_closure_tree
|
Love this feature. I can see the pattern used by many cases. The test need to assert that the hierarchy table was mutated, and that it work with the 3 adapters. |
|
@vsmay98 you will finish the pr ? |
|
Yeah @seuros , I haven’t had much time recently, but I’ll try to finish this PR soon. I was having some trouble running the MySQL specs locally and needed to look into it. |
|
Docker |
|
Thanks @seuros |
There was a problem hiding this 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 introduces a new dependent: :adopt option for has_closure_tree, enabling intermediate nodes to be removed while preserving tree structure by moving children to their grandparent. This fills a gap between the existing :nullify (which makes all children root nodes) and :destroy/:delete_all (which remove entire subtrees).
Key Changes:
- Added adoption logic that reassigns children to their grandparent (or makes them root nodes if no grandparent exists) before destroying a node
- Enhanced the Arel helpers with a
to_sql_with_connectionmethod to ensure proper SQL quoting across different database adapters - Comprehensive test coverage across PostgreSQL, MySQL, and SQLite databases
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/closure_tree/hierarchy_maintenance.rb | Implements adopt_children_to_grandparent method that updates children's parent_id and rebuilds their hierarchies before node destruction |
| lib/closure_tree/association_setup.rb | Maps :adopt to :nullify for the children association since ActiveRecord doesn't support :adopt natively, with adoption handled by callback |
| lib/closure_tree/arel_helpers.rb | Adds to_sql_with_connection helper to convert Arel queries with proper database-specific quoting |
| lib/closure_tree/support.rb | Updates documentation comment to include :adopt as a valid dependent option |
| README.md | Documents the new :adopt option with clear explanation of its behavior and use cases |
| test/closure_tree/adopt_test.rb | Comprehensive test suite covering adoption scenarios: grandparent adoption, root node destruction, multiple children, deep nesting, and integration with existing methods |
| test/dummy/app/models/adoptable_tag.rb | Test model for PostgreSQL using :adopt option |
| test/dummy/app/models/mysql_adoptable_tag.rb | Test model for MySQL using :adopt option |
| test/dummy/app/models/memory_adoptable_tag.rb | Test model for SQLite using :adopt option |
| test/dummy/db/schema.rb | Schema definitions for adoptable_tags test tables across all database types |
| test/dummy/db/sqlite_schema.rb | SQLite-specific schema for memory_adoptable_tags table |
| test/dummy/db/secondary_schema.rb | MySQL-specific schema for mysql_adoptable_tags table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has two blank lines at the end instead of one. Per the codebase convention, Ruby files should end with exactly one blank line. Remove the extra blank line.
| class MemoryAdoptableTag < SqliteRecord | ||
| has_closure_tree dependent: :adopt, name_column: 'name' | ||
| end | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has two blank lines at the end instead of one. Per the codebase convention, Ruby files should end with exactly one blank line. Remove the extra blank line.
| class MysqlAdoptableTag < MysqlRecord | ||
| has_closure_tree dependent: :adopt, name_column: 'name' | ||
| end | ||
|
|
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file has two blank lines at the end instead of one. Per the codebase convention, Ruby files should end with exactly one blank line. Remove the extra blank line.
| assert_equal p2, p3.parent | ||
|
|
||
| hierarchy = model_class.hierarchy_class | ||
| initial_p2_hierarchies = hierarchy.where(ancestor_id: p2.id).count |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to initial_p2_hierarchies is useless, since its value is never read.
| initial_p2_hierarchies = hierarchy.where(ancestor_id: p2.id).count |
|
|
||
| hierarchy = model_class.hierarchy_class | ||
| initial_p2_hierarchies = hierarchy.where(ancestor_id: p2.id).count | ||
| initial_p3_hierarchies = hierarchy.where(descendant_id: p3.id).count |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to initial_p3_hierarchies is useless, since its value is never read.
| initial_p3_hierarchies = hierarchy.where(descendant_id: p3.id).count |
| leaf = model_class.create!(name: 'leaf', parent: p2) | ||
|
|
||
| hierarchy = model_class.hierarchy_class | ||
| initial_count = hierarchy.count |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to initial_count is useless, since its value is never read.
| initial_count = hierarchy.count |
|
@vsmay98 pretty close, the linter and the unused code. |
Summary
This PR adds a new
dependent: :adoptoption tohas_closure_treethat automatically moves children to their grandparent when a parent node is destroyed. If the node being destroyed is a root (has no grandparent), children become root nodes instead.Motivation
When working with hierarchical data structures, there are scenarios where you want to remove intermediate nodes while preserving the tree structure. The existing
dependentoptions (:nullify,:destroy,:delete_all) don't provide this behavior::nullifymakes all children root nodes, which can break tree relationships:destroyand:delete_allremove the entire subtree, which may not be desiredThe
:adoptoption fills this gap by maintaining tree continuity when removing nodes, which is particularly useful for:Example Usage
Behavior
Implementation Details
Association Setup: The
has_many :childrenassociation uses:nullifywhendependent: :adoptis set (since ActiveRecord doesn't support:adoptdirectly), but the actual adoption logic is handled in thebefore_destroycallback.Adoption Logic: The
adopt_children_to_grandparentmethod:nilfor root nodes)parent_idto the grandparent IDOrder of Operations: Adoption happens before hierarchy references are deleted to ensure children can be properly identified and updated.
Backward Compatibility
This change is fully backward compatible. The new
:adoptoption is optional and doesn't affect existing behavior. All existingdependentoptions (:nullify,:destroy,:delete_all,nil) continue to work as before.