Skip to content

Conversation

@vsmay98
Copy link
Contributor

@vsmay98 vsmay98 commented Nov 22, 2025

Summary

This PR adds a new dependent: :adopt option to has_closure_tree that 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 dependent options (:nullify, :destroy, :delete_all) don't provide this behavior:

  • :nullify makes all children root nodes, which can break tree relationships
  • :destroy and :delete_all remove the entire subtree, which may not be desired

The :adopt option fills this gap by maintaining tree continuity when removing nodes, which is particularly useful for:

  • Category hierarchies: When removing a category, its subcategories should move up to the parent category
  • Organizational structures: When a department is dissolved, its teams should be reassigned to the parent department
  • File systems: When a folder is deleted, its contents should move to the parent folder
  • Menu structures: When a menu item is removed, its sub-items should be promoted to the parent level

Example Usage

class Category < ApplicationRecord
  has_closure_tree dependent: :adopt, name_column: 'name'
end

# Create a hierarchy: Electronics -> Computers -> Laptops -> Gaming Laptops
root = Category.create!(name: 'Electronics')
computers = Category.create!(name: 'Computers', parent: root)
laptops = Category.create!(name: 'Laptops', parent: computers)
gaming = Category.create!(name: 'Gaming Laptops', parent: laptops)

# Remove the intermediate "Laptops" category
laptops.destroy

# Gaming Laptops is now directly under Computers
gaming.reload
computers.reload
gaming.parent == computers  # => true
gaming.ancestry_path         # => ["Electronics", "Computers", "Gaming Laptops"]

Behavior

  • When destroying a node with a parent (grandparent exists): All children are moved to the grandparent
  • When destroying a root node (no grandparent): All children become root nodes
  • Hierarchy maintenance: The closure table is automatically rebuilt for each adopted child to maintain correct ancestor/descendant relationships
  • Deep nesting: Works correctly with deeply nested structures - only immediate children are adopted, maintaining their own subtree structure

Implementation Details

  1. Association Setup: The has_many :children association uses :nullify when dependent: :adopt is set (since ActiveRecord doesn't support :adopt directly), but the actual adoption logic is handled in the before_destroy callback.

  2. Adoption Logic: The adopt_children_to_grandparent method:

    • Retrieves the grandparent ID (or nil for root nodes)
    • Finds all children of the node being destroyed
    • Updates each child's parent_id to the grandparent ID
    • Rebuilds the hierarchy for each child to maintain closure table integrity
  3. Order 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 :adopt option is optional and doesn't affect existing behavior. All existing dependent options (:nullify, :destroy, :delete_all, nil) continue to work as before.

@vsmay98
Copy link
Contributor Author

vsmay98 commented Nov 22, 2025

@seuros
This PR is ready for review. Please take a look when you get a chance and share your thoughts. Thanks!

@seuros seuros changed the title Add dependent: :adopt option for has_closure_tree feat: Add dependent: :adopt option for has_closure_tree Nov 22, 2025
@seuros
Copy link
Member

seuros commented Nov 22, 2025

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.

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.

2 participants