loyal_awesome_nested_set 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|