Skip to content

Commit 254ba36

Browse files
authored
feat: sibling reordering when node changes parent or scope (#484)
1 parent c030385 commit 254ba36

File tree

12 files changed

+463
-51
lines changed

12 files changed

+463
-51
lines changed

lib/closure_tree/hierarchy_maintenance.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ def _ct_skip_cycle_detection!
1818
end
1919

2020
def _ct_skip_sort_order_maintenance!
21-
@_ct_skip_sort_order_maintenance = true
21+
ActiveSupport::Deprecation.new.warn(
22+
'_ct_skip_sort_order_maintenance! is deprecated and will be removed in the next major version. ' \
23+
'Sort order maintenance is now handled automatically.'
24+
)
2225
end
2326

2427
def _ct_validate
@@ -38,15 +41,24 @@ def _ct_before_save
3841
end
3942

4043
def _ct_after_save
41-
rebuild! if saved_changes[_ct.parent_column_name] || @was_new_record
44+
scope_changed = _ct.order_is_numeric? && _ct.scope_changed?(self)
45+
46+
if saved_changes[_ct.parent_column_name] || @was_new_record
47+
rebuild!
48+
elsif scope_changed
49+
# Scope changed without parent change - reorder old scope's siblings
50+
_ct_reorder_prior_siblings_if_parent_changed
51+
_ct_reorder_siblings
52+
elsif _ct.order_option? && saved_changes[_ct.order_column_sym]
53+
_ct_reorder_siblings(saved_changes[_ct.order_column_sym].min)
54+
end
4255
if saved_changes[_ct.parent_column_name] && !@was_new_record
4356
# Resetting the ancestral collections addresses
4457
# https://github.com/mceachen/closure_tree/issues/68
4558
ancestor_hierarchies.reload
4659
self_and_ancestors.reload
4760
end
4861
@was_new_record = false # we aren't new anymore.
49-
@_ct_skip_sort_order_maintenance = false # only skip once.
5062
true # don't cancel anything.
5163
end
5264

@@ -86,7 +98,7 @@ def rebuild!(called_by_rebuild = false)
8698
SQL
8799
end
88100

89-
if _ct.order_is_numeric? && !@_ct_skip_sort_order_maintenance
101+
if _ct.order_is_numeric?
90102
_ct_reorder_prior_siblings_if_parent_changed
91103
# Prevent double-reordering of siblings:
92104
_ct_reorder_siblings unless called_by_rebuild

lib/closure_tree/numeric_deterministic_ordering.rb

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ module NumericDeterministicOrdering
1212
end
1313

1414
def _ct_reorder_prior_siblings_if_parent_changed
15-
return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
15+
return if @was_new_record
16+
17+
parent_changed = saved_change_to_attribute?(_ct.parent_column_name)
18+
scope_changed = _ct.scope_changed?(self)
19+
20+
return unless parent_changed || scope_changed
1621

1722
was_parent_id = attribute_before_last_save(_ct.parent_column_name)
18-
scope_conditions = _ct.scope_values_from_instance(self)
19-
_ct.reorder_with_parent_id(was_parent_id, nil, scope_conditions)
23+
previous_scope_conditions = _ct.previous_scope_values_from_instance(self)
24+
_ct.reorder_with_parent_id(was_parent_id, nil, previous_scope_conditions)
2025
end
2126

2227
def _ct_reorder_siblings(minimum_sort_order_value = nil)
@@ -132,9 +137,7 @@ def append_child(child_node)
132137
def prepend_child(child_node)
133138
child_node.order_value = -1
134139
child_node.parent = self
135-
child_node._ct_skip_sort_order_maintenance!
136140
if child_node.save
137-
_ct_reorder_children
138141
child_node.reload
139142
else
140143
child_node
@@ -161,19 +164,11 @@ def add_sibling(sibling, add_after = true)
161164

162165
_ct.with_advisory_lock do
163166
prior_sibling_parent = sibling.parent
164-
reorder_from_value = if prior_sibling_parent == parent
165-
[order_value, sibling.order_value].compact.min
166-
else
167-
order_value
168-
end
169167

170168
sibling.order_value = order_value
171169
sibling.parent = parent
172-
sibling._ct_skip_sort_order_maintenance!
173170
sibling.save # may be a no-op
174171

175-
_ct_reorder_siblings(reorder_from_value)
176-
177172
# The sort order should be correct now except for self and sibling, which may need to flip:
178173
sibling_is_after = reload.order_value < sibling.reload.order_value
179174
if add_after != sibling_is_after

lib/closure_tree/support.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,45 @@ def scope_values_from_instance(instance)
257257
scope_hash
258258
end
259259

260+
def previous_scope_values_from_instance(instance)
261+
return {} unless options[:scope] && instance
262+
263+
scope_option = options[:scope]
264+
scope_hash = {}
265+
266+
case scope_option
267+
when Symbol
268+
value = instance.attribute_before_last_save(scope_option)
269+
scope_hash[scope_option] = value
270+
when Array
271+
scope_option.each do |item|
272+
if item.is_a?(Symbol)
273+
value = instance.attribute_before_last_save(item)
274+
scope_hash[item] = value
275+
end
276+
end
277+
end
278+
279+
scope_hash
280+
end
281+
282+
def scope_changed?(instance)
283+
return false unless options[:scope] && instance
284+
285+
scope_option = options[:scope]
286+
287+
case scope_option
288+
when Symbol
289+
instance.saved_change_to_attribute?(scope_option)
290+
when Array
291+
scope_option.any? do |item|
292+
item.is_a?(Symbol) && instance.saved_change_to_attribute?(item)
293+
end
294+
else
295+
false
296+
end
297+
end
298+
260299
def apply_scope_conditions(scope, instance = nil)
261300
return scope unless options[:scope] && instance
262301

test/closure_tree/label_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@ def create_preorder_tree(suffix = '')
7070
Label.roots.each_with_index do |root, root_idx|
7171
root.order_value = root_idx
7272
yield(root) if block_given?
73-
root.save!
73+
root.update_columns(root._ct.order_column => root_idx, type: root.type)
7474
root.self_and_descendants.each do |ea|
75-
ea.children.to_a.sort_by(&:name).each_with_index do |ea, idx|
76-
ea.order_value = idx
77-
yield(ea) if block_given?
78-
ea.save!
75+
ea.children.to_a.sort_by(&:name).each_with_index do |child, idx|
76+
child.order_value = idx
77+
yield(child) if block_given?
78+
child.update_columns(child._ct.order_column => idx, type: child.type)
7979
end
8080
end
8181
end

test/closure_tree/multi_database_test.rb

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,6 @@
33
require 'test_helper'
44

55
class MultiDatabaseTest < ActiveSupport::TestCase
6-
def setup
7-
super
8-
# Create memory tables - always recreate for clean state
9-
LiteRecord.connection.create_table :memory_tags, force: true do |t|
10-
t.string :name
11-
t.integer :parent_id
12-
t.timestamps
13-
end
14-
15-
LiteRecord.connection.create_table :memory_tag_hierarchies, id: false, force: true do |t|
16-
t.integer :ancestor_id, null: false
17-
t.integer :descendant_id, null: false
18-
t.integer :generations, null: false
19-
end
20-
21-
LiteRecord.connection.add_index :memory_tag_hierarchies, %i[ancestor_id descendant_id generations],
22-
unique: true, name: 'memory_tag_anc_desc_idx'
23-
LiteRecord.connection.add_index :memory_tag_hierarchies, [:descendant_id], name: 'memory_tag_desc_idx'
24-
end
25-
26-
def teardown
27-
# Clean up SQLite tables after each test
28-
LiteRecord.connection.drop_table :memory_tag_hierarchies, if_exists: true
29-
LiteRecord.connection.drop_table :memory_tags, if_exists: true
30-
super
31-
end
326

337
def test_postgresql_with_advisory_lock
348
skip 'PostgreSQL not configured' unless postgresql?(ApplicationRecord.connection)

0 commit comments

Comments
 (0)