Skip to content

Commit 7d82209

Browse files
author
Tom Smyth
committed
Added dont_order_roots option
1 parent 6fcfe12 commit 7d82209

File tree

8 files changed

+129
-27
lines changed

8 files changed

+129
-27
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,25 @@ root.reload.children.pluck(:name)
489489
=> ["b", "c", "a"]
490490
```
491491

492+
### Ordering Roots
493+
494+
With numeric ordering, root nodes are, by default, assigned order values globally across the whole database
495+
table. So for instance if you have 5 nodes with no parent, they will be ordered 0 through 4 by default.
496+
If your model represents many separate trees and you have a lot of records, this can cause performance
497+
problems, and doesn't really make much sense.
498+
499+
You can disable this default behavior by passing `dont_order_roots: true` as an option to your delcaration:
500+
501+
```
502+
has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true
503+
```
504+
505+
In this case, calling `prepend_sibling` and `append_sibling` on a root node or calling
506+
`roots_and_descendants_preordered` on the model will raise a `RootOrderingDisabledError`.
507+
508+
The `dont_order_roots` option will be ignored unless `numeric_order` is set to true.
509+
510+
492511
## Concurrency
493512
494513
Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.

lib/closure_tree/has_closure_tree.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def has_closure_tree(options = {})
88
:hierarchy_table_name,
99
:name_column,
1010
:order,
11+
:dont_order_roots,
1112
:numeric_order,
1213
:touch,
1314
:with_advisory_lock

lib/closure_tree/has_closure_tree_root.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module ClosureTree
22
class MultipleRootError < StandardError; end
3+
class RootOrderingDisabledError < StandardError; end
34

45
module HasClosureTreeRoot
56

lib/closure_tree/numeric_deterministic_ordering.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,15 @@ def _ct_sum_order_by(node = nil)
6565
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
6666
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
6767

68-
Arel.sql("SUM(#{node_score})")
68+
# We want the NULLs to be first in case we are not ordering roots and they have NULL order.
69+
Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
6970
end
7071

7172
def roots_and_descendants_preordered
73+
if _ct.dont_order_roots
74+
raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
75+
end
76+
7277
join_sql = <<-SQL.strip_heredoc
7378
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
7479
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
@@ -95,8 +100,12 @@ def prepend_child(child_node)
95100
child_node.parent = self
96101
child_node._ct_skip_sort_order_maintenance!
97102
if child_node.save
103+
puts 'Prepending'
104+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
98105
_ct_reorder_children
106+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
99107
child_node.reload
108+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
100109
else
101110
child_node
102111
end
@@ -113,6 +122,10 @@ def prepend_sibling(sibling_node)
113122
def add_sibling(sibling, add_after = true)
114123
fail "can't add self as sibling" if self == sibling
115124

125+
if _ct.dont_order_roots && parent.nil?
126+
raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
127+
end
128+
116129
# Make sure self isn't dirty, because we're going to call reload:
117130
save
118131

lib/closure_tree/numeric_order_support.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def self.adapter_for_connection(connection)
1414

1515
module MysqlAdapter
1616
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
17+
return if parent_id.nil? && dont_order_roots
1718
min_where = if minimum_sort_order_value
1819
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
1920
else
@@ -31,6 +32,7 @@ def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
3132

3233
module PostgreSQLAdapter
3334
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
35+
return if parent_id.nil? && dont_order_roots
3436
min_where = if minimum_sort_order_value
3537
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
3638
else
@@ -56,6 +58,7 @@ def rows_updated(result)
5658

5759
module GenericAdapter
5860
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
61+
return if parent_id.nil? && dont_order_roots
5962
scope = model_class.
6063
where(parent_column_sym => parent_id).
6164
order(nulls_last_order_by)

lib/closure_tree/support_attributes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def order_by
7575
options[:order]
7676
end
7777

78+
def dont_order_roots
79+
options[:dont_order_roots] || false
80+
end
81+
7882
def nulls_last_order_by
7983
"-#{quoted_order_column} #{order_by_order(reverse = true)}"
8084
end

spec/db/models.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ class DateLabel < Label
9999
class DirectoryLabel < Label
100100
end
101101

102+
class LabelWithoutRootOrdering < ActiveRecord::Base
103+
# make sure order doesn't matter
104+
acts_as_tree :order => :column_whereby_ordering_is_inferred, # <- symbol, and not "sort_order"
105+
:numeric_order => true,
106+
:dont_order_roots => true,
107+
:parent_column_name => "mother_id",
108+
:hierarchy_table_name => "label_hierarchies"
109+
110+
self.table_name = "#{table_name_prefix}labels#{table_name_suffix}"
111+
112+
def to_s
113+
"#{self.class}: #{name}"
114+
end
115+
end
116+
102117
class CuisineType < ActiveRecord::Base
103118
acts_as_tree
104119
end

spec/label_spec.rb

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,49 @@ def roots_name_and_order
289289
end
290290
end
291291

292+
context "doesn't order roots when requested" do
293+
before :each do
294+
@root1 = LabelWithoutRootOrdering.create!(:name => 'root1')
295+
@root2 = LabelWithoutRootOrdering.create!(:name => 'root2')
296+
@a, @b, @c, @d, @e = ('a'..'e').map { |ea| LabelWithoutRootOrdering.new(:name => ea) }
297+
@root1.children << @a
298+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
299+
@root1.append_child(@c)
300+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
301+
@root1.prepend_child(@d)
302+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
303+
@a.append_sibling(@b)
304+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
305+
@d.prepend_sibling(@e)
306+
p LabelWithoutRootOrdering.all.map { |l| [l.name, l.order_value] }
307+
end
308+
309+
it 'order_values properly' do
310+
expect(@root1.reload.order_value).to be_nil
311+
orders_and_names = @root1.children.reload.map { |ea| [ea.name, ea.order_value] }
312+
expect(orders_and_names).to eq([['e', 0], ['d', 1], ['a', 2], ['b', 3], ['c', 4]])
313+
end
314+
315+
it 'raises on prepending and appending to root' do
316+
expect { @root1.prepend_sibling(@f) }.to raise_error(ClosureTree::RootOrderingDisabledError)
317+
expect { @root1.append_sibling(@f) }.to raise_error(ClosureTree::RootOrderingDisabledError)
318+
end
319+
320+
it 'returns empty array for siblings_before and after' do
321+
expect(@root1.siblings_before).to eq([])
322+
expect(@root1.siblings_after).to eq([])
323+
end
324+
325+
it 'returns expected result for self_and_descendants_preordered' do
326+
expect(@root1.self_and_descendants_preordered.to_a).to eq([@root1, @e, @d, @a, @b, @c])
327+
end unless sqlite? # sqlite doesn't have a power function.
328+
329+
it 'raises on roots_and_descendants_preordered' do
330+
expect { LabelWithoutRootOrdering.roots_and_descendants_preordered }.to raise_error(
331+
ClosureTree::RootOrderingDisabledError)
332+
end
333+
end
334+
292335
describe 'code in the readme' do
293336
it 'creates STI label hierarchies' do
294337
child = Label.find_or_create_by_path([
@@ -341,17 +384,7 @@ def roots_name_and_order
341384
root = Label.create(:name => "root")
342385
a = Label.create(:name => "a", :parent => root)
343386
b = Label.create(:name => "b", :parent => root)
344-
expect(a.order_value).to eq(0)
345-
expect(b.order_value).to eq(1)
346-
#c = Label.create(:name => "c")
347387

348-
# should the order_value for roots be set?
349-
expect(root.order_value).not_to be_nil
350-
expect(root.order_value).to eq(0)
351-
352-
# order_value should never be nil on a child.
353-
expect(a.order_value).not_to be_nil
354-
expect(a.order_value).to eq(0)
355388
# Add a child to root at end of children.
356389
root.children << b
357390
expect(b.parent).to eq(root)
@@ -395,28 +428,41 @@ def roots_name_and_order
395428
end
396429

397430
context "order_value must be set" do
431+
shared_examples_for "correct order_value" do
432+
before do
433+
@root = model.create(name: 'root')
434+
@a, @b, @c = %w(a b c).map { |n| @root.children.create(name: n) }
435+
end
398436

399-
before do
400-
@root = Label.create(name: 'root')
401-
@a, @b, @c = %w(a b c).map { |n| @root.children.create(name: n) }
402-
end
437+
it 'should set order_value on roots' do
438+
expect(@root.order_value).to eq(expected_root_order_value)
439+
end
403440

404-
it 'should set order_value on roots' do
405-
expect(@root.order_value).to eq(0)
441+
it 'should set order_value with siblings' do
442+
expect(@a.order_value).to eq(0)
443+
expect(@b.order_value).to eq(1)
444+
expect(@c.order_value).to eq(2)
445+
end
446+
447+
it 'should reset order_value when a node is moved to another location' do
448+
root2 = model.create(name: 'root2')
449+
root2.add_child @b
450+
expect(@a.order_value).to eq(0)
451+
expect(@b.order_value).to eq(0)
452+
expect(@c.reload.order_value).to eq(1)
453+
end
406454
end
407455

408-
it 'should set order_value with siblings' do
409-
expect(@a.order_value).to eq(0)
410-
expect(@b.order_value).to eq(1)
411-
expect(@c.order_value).to eq(2)
456+
context "with normal model" do
457+
let(:model) { Label }
458+
let(:expected_root_order_value) { 0 }
459+
it_behaves_like "correct order_value"
412460
end
413461

414-
it 'should reset order_value when a node is moved to another location' do
415-
root2 = Label.create(name: 'root2')
416-
root2.add_child @b
417-
expect(@a.order_value).to eq(0)
418-
expect(@b.order_value).to eq(0)
419-
expect(@c.reload.order_value).to eq(1)
462+
context "without root ordering" do
463+
let(:model) { LabelWithoutRootOrdering }
464+
let(:expected_root_order_value) { nil }
465+
it_behaves_like "correct order_value"
420466
end
421467
end
422468

0 commit comments

Comments
 (0)