loyal_awesome_nested_set 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +57 -0
- data/MIT-LICENSE +20 -0
- data/README.md +163 -0
- data/lib/awesome_nested_set.rb +8 -0
- data/lib/awesome_nested_set/awesome_nested_set.rb +134 -0
- data/lib/awesome_nested_set/columns.rb +72 -0
- data/lib/awesome_nested_set/helper.rb +44 -0
- data/lib/awesome_nested_set/iterator.rb +29 -0
- data/lib/awesome_nested_set/model.rb +212 -0
- data/lib/awesome_nested_set/model/movable.rb +137 -0
- data/lib/awesome_nested_set/model/prunable.rb +58 -0
- data/lib/awesome_nested_set/model/rebuildable.rb +40 -0
- data/lib/awesome_nested_set/model/relatable.rb +121 -0
- data/lib/awesome_nested_set/model/transactable.rb +27 -0
- data/lib/awesome_nested_set/model/validatable.rb +69 -0
- data/lib/awesome_nested_set/move.rb +117 -0
- data/lib/awesome_nested_set/set_validator.rb +63 -0
- data/lib/awesome_nested_set/tree.rb +63 -0
- data/lib/awesome_nested_set/version.rb +3 -0
- metadata +151 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'awesome_nested_set/tree'
|
2
|
+
|
3
|
+
module CollectiveIdea
|
4
|
+
module Acts
|
5
|
+
module NestedSet
|
6
|
+
module Model
|
7
|
+
module Rebuildable
|
8
|
+
|
9
|
+
|
10
|
+
# Rebuilds the left & rights if unset or invalid.
|
11
|
+
# Also very useful for converting from acts_as_tree.
|
12
|
+
def rebuild!(validate_nodes = true)
|
13
|
+
# default_scope with order may break database queries so we do all operation without scope
|
14
|
+
unscoped do
|
15
|
+
Tree.new(self, validate_nodes).rebuild!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def scope_for_rebuild
|
20
|
+
scope = proc {}
|
21
|
+
|
22
|
+
if acts_as_nested_set_options[:scope]
|
23
|
+
scope = proc {|node|
|
24
|
+
scope_column_names.inject("") {|str, column_name|
|
25
|
+
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name))} "
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
scope
|
30
|
+
end
|
31
|
+
|
32
|
+
def order_for_rebuild
|
33
|
+
"#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module CollectiveIdea
|
2
|
+
module Acts
|
3
|
+
module NestedSet
|
4
|
+
module Model
|
5
|
+
module Relatable
|
6
|
+
|
7
|
+
# Returns an collection of all parents
|
8
|
+
def ancestors
|
9
|
+
without_self self_and_ancestors
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the collection of all parents and self
|
13
|
+
def self_and_ancestors
|
14
|
+
nested_set_scope.
|
15
|
+
where(arel_table[left_column_name].lteq(left)).
|
16
|
+
where(arel_table[right_column_name].gteq(right))
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the collection of all children of the parent, except self
|
20
|
+
def siblings
|
21
|
+
without_self self_and_siblings
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the collection of all children of the parent, including self
|
25
|
+
def self_and_siblings
|
26
|
+
nested_set_scope.children_of parent_id
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a set of all of its nested children which do not have children
|
30
|
+
def leaves
|
31
|
+
descendants.where(
|
32
|
+
"#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the level of this object in the tree
|
37
|
+
# root level is 0
|
38
|
+
def level
|
39
|
+
parent_id.nil? ? 0 : compute_level
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a collection including all of its children and nested children
|
43
|
+
def descendants
|
44
|
+
without_self self_and_descendants
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a collection including itself and all of its nested children
|
48
|
+
def self_and_descendants
|
49
|
+
# using _left_ for both sides here lets us benefit from an index on that column if one exists
|
50
|
+
nested_set_scope.right_of(left).left_of(right)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_descendant_of?(other)
|
54
|
+
within_node?(other, self) && same_scope?(other)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_or_is_descendant_of?(other)
|
58
|
+
(other == self || within_node?(other, self)) && same_scope?(other)
|
59
|
+
end
|
60
|
+
|
61
|
+
def is_ancestor_of?(other)
|
62
|
+
within_node?(self, other) && same_scope?(other)
|
63
|
+
end
|
64
|
+
|
65
|
+
def is_or_is_ancestor_of?(other)
|
66
|
+
(self == other || within_node?(self, other)) && same_scope?(other)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if other model is in the same scope
|
70
|
+
def same_scope?(other)
|
71
|
+
Array(acts_as_nested_set_options[:scope]).all? do |attr|
|
72
|
+
self.send(attr) == other.send(attr)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Find the first sibling to the left
|
77
|
+
def left_sibling
|
78
|
+
siblings.left_of(left).last
|
79
|
+
end
|
80
|
+
|
81
|
+
# Find the first sibling to the right
|
82
|
+
def right_sibling
|
83
|
+
siblings.right_of(left).first
|
84
|
+
end
|
85
|
+
|
86
|
+
def root
|
87
|
+
return self_and_ancestors.children_of(nil).first if persisted?
|
88
|
+
|
89
|
+
if parent_id && current_parent = nested_set_scope.find(parent_id)
|
90
|
+
current_parent.root
|
91
|
+
else
|
92
|
+
self
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
protected
|
97
|
+
|
98
|
+
def compute_level
|
99
|
+
node, nesting = determine_depth
|
100
|
+
|
101
|
+
node == self ? ancestors.count : node.level + nesting
|
102
|
+
end
|
103
|
+
|
104
|
+
def determine_depth(node = self, nesting = 0)
|
105
|
+
while (association = node.association(:parent)).loaded? && association.target
|
106
|
+
nesting += 1
|
107
|
+
node = node.parent
|
108
|
+
end if node.respond_to?(:association)
|
109
|
+
|
110
|
+
[node, nesting]
|
111
|
+
end
|
112
|
+
|
113
|
+
def within_node?(node, within)
|
114
|
+
node.left < within.left && within.left < node.right
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
module Model
|
5
|
+
module Transactable
|
6
|
+
|
7
|
+
protected
|
8
|
+
def in_tenacious_transaction(&block)
|
9
|
+
retry_count = 0
|
10
|
+
begin
|
11
|
+
transaction(&block)
|
12
|
+
rescue ActiveRecord::StatementInvalid => error
|
13
|
+
raise unless connection.open_transactions.zero?
|
14
|
+
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
|
15
|
+
raise unless retry_count < 10
|
16
|
+
retry_count += 1
|
17
|
+
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
|
18
|
+
sleep(rand(retry_count)*0.1) # Aloha protocol
|
19
|
+
retry
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'awesome_nested_set/set_validator'
|
2
|
+
|
3
|
+
module CollectiveIdea
|
4
|
+
module Acts
|
5
|
+
module NestedSet
|
6
|
+
module Model
|
7
|
+
module Validatable
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
|
11
|
+
end
|
12
|
+
|
13
|
+
def left_and_rights_valid?
|
14
|
+
SetValidator.new(self).valid?
|
15
|
+
end
|
16
|
+
|
17
|
+
def no_duplicates_for_columns?
|
18
|
+
[quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
|
19
|
+
# No duplicates
|
20
|
+
select("#{scope_string}#{column}, COUNT(#{column})").
|
21
|
+
group("#{scope_string}#{column}", quoted_primary_key_column_full_name).
|
22
|
+
having("COUNT(#{column}) > 1").
|
23
|
+
first.nil?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Wrapper for each_root_valid? that can deal with scope.
|
28
|
+
def all_roots_valid?
|
29
|
+
if acts_as_nested_set_options[:scope]
|
30
|
+
all_roots_valid_by_scope?(roots)
|
31
|
+
else
|
32
|
+
each_root_valid?(roots)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def all_roots_valid_by_scope?(roots_to_validate)
|
37
|
+
roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
|
38
|
+
each_root_valid?(grouped_roots)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def each_root_valid?(roots_to_validate)
|
43
|
+
left = right = 0
|
44
|
+
roots_to_validate.all? do |root|
|
45
|
+
(root.left > left && root.right > right).tap do
|
46
|
+
left = root.left
|
47
|
+
right = root.right
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def roots_grouped_by_scope(roots_to_group)
|
54
|
+
roots_to_group.group_by {|record|
|
55
|
+
scope_column_names.collect {|col| record.send(col) }
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def scope_string
|
60
|
+
Array(acts_as_nested_set_options[:scope]).map do |c|
|
61
|
+
connection.quote_column_name(c)
|
62
|
+
end.push(nil).join(", ")
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
class Move
|
5
|
+
attr_reader :target, :position, :instance
|
6
|
+
|
7
|
+
def initialize(target, position, instance)
|
8
|
+
@target = target
|
9
|
+
@position = position
|
10
|
+
@instance = instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def move
|
14
|
+
prevent_impossible_move
|
15
|
+
|
16
|
+
bound, other_bound = get_boundaries
|
17
|
+
|
18
|
+
# there would be no change
|
19
|
+
return if bound == right || bound == left
|
20
|
+
|
21
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
22
|
+
# so sorting puts both the intervals and their boundaries in order
|
23
|
+
a, b, c, d = [left, right, bound, other_bound].sort
|
24
|
+
|
25
|
+
lock_nodes_between! a, d
|
26
|
+
|
27
|
+
nested_set_scope.where(where_statement(a, d)).
|
28
|
+
update_all(conditions(a, b, c, d))
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
delegate :left, :right, :left_column_name, :right_column_name,
|
34
|
+
:quoted_left_column_name, :quoted_right_column_name,
|
35
|
+
:quoted_parent_column_name, :parent_column_name, :nested_set_scope,
|
36
|
+
:to => :instance
|
37
|
+
|
38
|
+
delegate :arel_table, :class, :to => :instance, :prefix => true
|
39
|
+
delegate :base_class, :to => :instance_class, :prefix => :instance
|
40
|
+
|
41
|
+
def where_statement(left_bound, right_bound)
|
42
|
+
instance_arel_table[left_column_name].in(left_bound..right_bound).
|
43
|
+
or(instance_arel_table[right_column_name].in(left_bound..right_bound))
|
44
|
+
end
|
45
|
+
|
46
|
+
def conditions(a, b, c, d)
|
47
|
+
[
|
48
|
+
case_condition_for_direction(:quoted_left_column_name) +
|
49
|
+
case_condition_for_direction(:quoted_right_column_name) +
|
50
|
+
case_condition_for_parent,
|
51
|
+
{:a => a, :b => b, :c => c, :d => d, :id => instance.id, :new_parent => new_parent}
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
def case_condition_for_direction(column_name)
|
56
|
+
column = send(column_name)
|
57
|
+
"#{column} = CASE " +
|
58
|
+
"WHEN #{column} BETWEEN :a AND :b " +
|
59
|
+
"THEN #{column} + :d - :b " +
|
60
|
+
"WHEN #{column} BETWEEN :c AND :d " +
|
61
|
+
"THEN #{column} + :a - :c " +
|
62
|
+
"ELSE #{column} END, "
|
63
|
+
end
|
64
|
+
|
65
|
+
def case_condition_for_parent
|
66
|
+
"#{quoted_parent_column_name} = CASE " +
|
67
|
+
"WHEN #{instance_base_class.primary_key} = :id THEN :new_parent " +
|
68
|
+
"ELSE #{quoted_parent_column_name} END"
|
69
|
+
end
|
70
|
+
|
71
|
+
def lock_nodes_between!(left_bound, right_bound)
|
72
|
+
# select the rows in the model between a and d, and apply a lock
|
73
|
+
instance_base_class.right_of(left_bound).left_of_right_side(right_bound).
|
74
|
+
select(:id).lock(true)
|
75
|
+
end
|
76
|
+
|
77
|
+
def root
|
78
|
+
position == :root
|
79
|
+
end
|
80
|
+
|
81
|
+
def new_parent
|
82
|
+
case position
|
83
|
+
when :child
|
84
|
+
target.id
|
85
|
+
else
|
86
|
+
target[parent_column_name]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_boundaries
|
91
|
+
if (bound = target_bound) > right
|
92
|
+
bound -= 1
|
93
|
+
other_bound = right + 1
|
94
|
+
else
|
95
|
+
other_bound = left - 1
|
96
|
+
end
|
97
|
+
[bound, other_bound]
|
98
|
+
end
|
99
|
+
|
100
|
+
def prevent_impossible_move
|
101
|
+
if !root && !instance.move_possible?(target)
|
102
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def target_bound
|
107
|
+
case position
|
108
|
+
when :child; right(target)
|
109
|
+
when :left; left(target)
|
110
|
+
when :right; right(target) + 1
|
111
|
+
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
class SetValidator
|
5
|
+
|
6
|
+
def initialize(model)
|
7
|
+
@model = model
|
8
|
+
@scope = model.all
|
9
|
+
@parent = arel_table.alias('parent')
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
query.count == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :model, :parent
|
19
|
+
attr_accessor :scope
|
20
|
+
|
21
|
+
delegate :parent_column_name, :primary_key, :left_column_name, :right_column_name, :arel_table,
|
22
|
+
:quoted_table_name, :quoted_parent_column_full_name, :quoted_left_column_full_name, :quoted_right_column_full_name, :quoted_left_column_name, :quoted_right_column_name,
|
23
|
+
:to => :model
|
24
|
+
|
25
|
+
def query
|
26
|
+
join_scope
|
27
|
+
filter_scope
|
28
|
+
end
|
29
|
+
|
30
|
+
def join_scope
|
31
|
+
join_arel = arel_table.join(parent, Arel::Nodes::OuterJoin).on(parent[primary_key].eq(arel_table[parent_column_name]))
|
32
|
+
self.scope = scope.joins(join_arel.join_sql)
|
33
|
+
end
|
34
|
+
|
35
|
+
def filter_scope
|
36
|
+
self.scope = scope.where(
|
37
|
+
bound_is_null(left_column_name).
|
38
|
+
or(bound_is_null(right_column_name)).
|
39
|
+
or(left_bound_greater_than_right).
|
40
|
+
or(parent_not_null.and(bounds_outside_parent))
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def bound_is_null(column_name)
|
45
|
+
arel_table[column_name].eq(nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def left_bound_greater_than_right
|
49
|
+
arel_table[left_column_name].gteq(arel_table[right_column_name])
|
50
|
+
end
|
51
|
+
|
52
|
+
def parent_not_null
|
53
|
+
arel_table[parent_column_name].not_eq(nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
def bounds_outside_parent
|
57
|
+
arel_table[left_column_name].lteq(parent[left_column_name]).or(arel_table[right_column_name].gteq(parent[right_column_name]))
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module Acts #:nodoc:
|
3
|
+
module NestedSet #:nodoc:
|
4
|
+
class Tree
|
5
|
+
attr_reader :model, :validate_nodes
|
6
|
+
attr_accessor :indices
|
7
|
+
|
8
|
+
delegate :left_column_name, :right_column_name, :quoted_parent_column_full_name,
|
9
|
+
:order_for_rebuild, :scope_for_rebuild,
|
10
|
+
:to => :model
|
11
|
+
|
12
|
+
def initialize(model, validate_nodes)
|
13
|
+
@model = model
|
14
|
+
@validate_nodes = validate_nodes
|
15
|
+
@indices = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def rebuild!
|
19
|
+
# Don't rebuild a valid tree.
|
20
|
+
return true if model.valid?
|
21
|
+
|
22
|
+
root_nodes.each do |root_node|
|
23
|
+
# setup index for this scope
|
24
|
+
indices[scope_for_rebuild.call(root_node)] ||= 0
|
25
|
+
set_left_and_rights(root_node)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def increment_indice!(node)
|
32
|
+
indices[scope_for_rebuild.call(node)] += 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_left_and_rights(node)
|
36
|
+
set_left!(node)
|
37
|
+
# find
|
38
|
+
node_children(node).each { |n| set_left_and_rights(n) }
|
39
|
+
set_right!(node)
|
40
|
+
|
41
|
+
node.save!(:validate => validate_nodes)
|
42
|
+
end
|
43
|
+
|
44
|
+
def node_children(node)
|
45
|
+
model.where(["#{quoted_parent_column_full_name} = ? #{scope_for_rebuild.call(node)}", node]).
|
46
|
+
order(order_for_rebuild)
|
47
|
+
end
|
48
|
+
|
49
|
+
def root_nodes
|
50
|
+
model.where("#{quoted_parent_column_full_name} IS NULL").order(order_for_rebuild)
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_left!(node)
|
54
|
+
node[left_column_name] = increment_indice!(node)
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_right!(node)
|
58
|
+
node[right_column_name] = increment_indice!(node)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|