Skip to content

Dirty tracking lost when setting parent with numeric_order option #414

Open

Description

@ryanb

Say I have a model with numeric_order option:

class Category < ApplicationRecord
  has_closure_tree order: "position", numeric_order: true
end

With this setting we lose dirty tracking after setting the parent:

category.update! parent: parent_category, name: "Foo"
category.saved_change_to_name? # => false

All dirty tracking is lost since it ends up calling reload internally.

Perhaps this code could just reload the order attribute instead of the entire record? This way dirty tracking would persist.

Activity

  1. ryanb commented on Jan 24, 2023

    @ryanb
    Author

    For the record, I'm currently monkey-patching to work around this issue. Feel free to use this in the implementation.

    # in an initializer:
    module ClosureTree
      module NumericDeterministicOrdering
        def _ct_reorder_siblings(minimum_sort_order_value = nil)
          _ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value)
          if !destroyed? && _ct.order_column
            self[_ct.order_column] = self.class
              .where(self.class.primary_key => id_in_database)
              .pick(_ct.order_column)
            clear_attribute_change(_ct.order_column)
          end
        end
      end
    end
  2. ihamzatarar commented on Nov 6, 2026

    @ihamzatarar

    @ryanb opened a PR built on your patch. Thank you for leaving it here, it saved most of the work.

    Three small changes from your version:

    • reorder_with_parent_id takes a third argument now, scope_conditions, added in Add scope option for root nodes and sibling ordering #466. Passing only two would silently drop scope filtering for ScopedItem and MultiScopedItem, so I left master's call as it is.
    • Dropped the && _ct.order_column guard. NumericDeterministicOrdering is only included when _ct.order_is_numeric?, so the column is always there.
    • Used the library's own order_value= writer and _ct.order_column_sym to match the rest of the file.

    I also checked that clear_attribute_change is safe on a record that has just been saved. It is mutations_from_database.forget_change(...), so it clears only the pre-save tracker and never touches mutations_before_last_save, which is what saved_change_to_* reads. Without it the record is left changed? with a pending write to a column whose value is already in the database.

    The other two reorder methods do not need the same treatment. _ct_reorder_children renumbers rows under the receiver and cannot change the receiver's own order column, and _ct_reorder_prior_siblings_if_parent_changed renumbers under the previous parent, which the receiver has already left by the time it runs.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions