acts_as_nested_interval 0.0.2 → 0.0.4
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/README.md +15 -6
- data/lib/acts_as_nested_interval/class_methods.rb +35 -0
- data/lib/acts_as_nested_interval/instance_methods.rb +220 -0
- data/lib/acts_as_nested_interval/version.rb +1 -1
- data/lib/acts_as_nested_interval.rb +43 -282
- data/test/dummy/log/test.log +2820 -0
- metadata +8 -6
data/README.md
CHANGED
@@ -2,14 +2,16 @@
|
|
2
2
|
|
3
3
|
## About
|
4
4
|
|
5
|
-
|
6
|
-
* This: https://github.com/clyfe/acts_as_nested_interval
|
7
|
-
* Original: https://github.com/pythonic/acts_as_nested_interval
|
8
|
-
* Acknowledgement: http://arxiv.org/html/cs.DB/0401014 by Vadim Tropashko.
|
5
|
+
Pythonic's acts_as_nested_interval updated to Rails 3 and gemified.
|
9
6
|
|
10
7
|
This act implements a nested-interval tree. You can find all descendants or all
|
11
8
|
ancestors with just one select query. You can insert and delete records without
|
12
|
-
a full table update.
|
9
|
+
a full table update (compared to nested set, where at insert, half the table is updated on average).
|
10
|
+
|
11
|
+
Nested sets/intervals are good if you need to sort in preorder at DB-level.
|
12
|
+
If you don't need that give a look to https://github.com/stefankroes/ancestry ,
|
13
|
+
that implements a simpler encoding model (variant of materialized path).
|
14
|
+
|
13
15
|
|
14
16
|
## Install
|
15
17
|
|
@@ -166,6 +168,13 @@ Region.rebuild_nested_interval_tree!
|
|
166
168
|
```
|
167
169
|
|
168
170
|
NOTE! About `rebuild_nested_interval_tree!`:
|
171
|
+
|
169
172
|
* zeroes all your tree intervals before recomputing them!
|
170
173
|
* does a lot of N+1 queries of type `record.parent` and not only.
|
171
|
-
This might change once the AR identity_map is finished.
|
174
|
+
This might change once the AR identity_map is finished.
|
175
|
+
|
176
|
+
## Authors
|
177
|
+
|
178
|
+
* This: https://github.com/clyfe/acts_as_nested_interval
|
179
|
+
* Original: https://github.com/pythonic/acts_as_nested_interval
|
180
|
+
* Acknowledgement: http://arxiv.org/html/cs.DB/0401014 by Vadim Tropashko.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActsAsNestedInterval
|
2
|
+
module ClassMethods
|
3
|
+
|
4
|
+
def rebuild_nested_interval_tree!
|
5
|
+
# temporary changes
|
6
|
+
skip_callback :update, :before, :update_nested_interval
|
7
|
+
skip_callback :update, :before, :sync_childre
|
8
|
+
old_default_scopes = default_scopes # save to revert later
|
9
|
+
default_scope where("#{quoted_table_name}.lftq > 0") # use lft1 > 0 as a "migrated?" flag
|
10
|
+
|
11
|
+
# zero all intervals
|
12
|
+
update_hash = {lftp: 0, lftq: 0}
|
13
|
+
update_hash[:rgtp] = 0 if columns_hash["rgtp"]
|
14
|
+
update_hash[:rgtq] = 0 if columns_hash["rgtq"]
|
15
|
+
update_hash[:lft] = 0 if columns_hash["lft"]
|
16
|
+
update_hash[:rgt] = 0 if columns_hash["rgt"]
|
17
|
+
update_all update_hash
|
18
|
+
|
19
|
+
# recompute intervals
|
20
|
+
clear_cache!
|
21
|
+
update_subtree = lambda { |node|
|
22
|
+
node.create_nested_interval
|
23
|
+
node.save
|
24
|
+
node.class.unscoped.where(nested_interval_foreign_key => node.id).find_each &update_subtree
|
25
|
+
}
|
26
|
+
unscoped.roots.find_each &update_subtree
|
27
|
+
|
28
|
+
# revert changes
|
29
|
+
set_callback :update, :before, :update_nested_interval
|
30
|
+
set_callback :update, :before, :sync_childre
|
31
|
+
self.default_scopes = old_default_scopes
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module ActsAsNestedInterval
|
2
|
+
module InstanceMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# selectively define #descendants according to table features
|
6
|
+
included do
|
7
|
+
|
8
|
+
if columns_hash["lft"]
|
9
|
+
|
10
|
+
def descendants
|
11
|
+
quoted_table_name = self.class.quoted_table_name
|
12
|
+
nested_interval_scope.where <<-SQL
|
13
|
+
#{lftp} < #{quoted_table_name}.lftp AND
|
14
|
+
#{quoted_table_name}.lft BETWEEN #{1.0 * lftp / lftq} AND #{1.0 * rgtp / rgtq}
|
15
|
+
SQL
|
16
|
+
end
|
17
|
+
|
18
|
+
elsif nested_interval_lft_index
|
19
|
+
|
20
|
+
def descendants
|
21
|
+
quoted_table_name = self.class.quoted_table_name
|
22
|
+
nested_interval_scope.where <<-SQL
|
23
|
+
#{lftp} < #{quoted_table_name}.lftp AND
|
24
|
+
1.0 * #{quoted_table_name}.lftp / #{quoted_table_name}.lftq BETWEEN
|
25
|
+
#{1.0 * lftp / lftq} AND
|
26
|
+
#{1.0 * rgtp / rgtq}
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
|
30
|
+
elsif connection.adapter_name == "MySQL"
|
31
|
+
|
32
|
+
def descendants
|
33
|
+
quoted_table_name = self.class.quoted_table_name
|
34
|
+
nested_interval_scope.where <<-SQL
|
35
|
+
( #{quoted_table_name}.lftp != #{rgtp} OR
|
36
|
+
#{quoted_table_name}.lftq != #{rgtq}
|
37
|
+
) AND
|
38
|
+
#{quoted_table_name}.lftp BETWEEN
|
39
|
+
1 + #{quoted_table_name}.lftq * #{lftp} DIV #{lftq} AND
|
40
|
+
#{quoted_table_name}.lftq * #{rgtp} DIV #{rgtq}
|
41
|
+
SQL
|
42
|
+
end
|
43
|
+
|
44
|
+
else
|
45
|
+
|
46
|
+
def descendants
|
47
|
+
quoted_table_name = self.class.quoted_table_name
|
48
|
+
nested_interval_scope.where <<-SQL
|
49
|
+
( #{quoted_table_name}.lftp != #{rgtp} OR
|
50
|
+
#{quoted_table_name}.lftq != #{rgtq}
|
51
|
+
) AND
|
52
|
+
#{quoted_table_name}.lftp BETWEEN
|
53
|
+
1 + #{quoted_table_name}.lftq * CAST(#{lftp} AS BIGINT) / #{lftq} AND
|
54
|
+
#{quoted_table_name}.lftq * CAST(#{rgtp} AS BIGINT) / #{rgtq}
|
55
|
+
SQL
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_nested_interval(lftp, lftq)
|
63
|
+
self.lftp, self.lftq = lftp, lftq
|
64
|
+
self.rgtp = rgtp if has_attribute?(:rgtp)
|
65
|
+
self.rgtq = rgtq if has_attribute?(:rgtq)
|
66
|
+
self.lft = lft if has_attribute?(:lft)
|
67
|
+
self.rgt = rgt if has_attribute?(:rgt)
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_nested_interval_for_top
|
71
|
+
if self.class.virtual_root
|
72
|
+
set_nested_interval *next_root_lft
|
73
|
+
else
|
74
|
+
set_nested_interval 0, 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Creates record.
|
79
|
+
def create_nested_interval
|
80
|
+
if read_attribute(nested_interval_foreign_key).nil?
|
81
|
+
set_nested_interval_for_top
|
82
|
+
else
|
83
|
+
set_nested_interval *parent.lock!.next_child_lft
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Destroys record.
|
88
|
+
def destroy_nested_interval
|
89
|
+
lock! rescue nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def nested_interval_scope
|
93
|
+
conditions = {}
|
94
|
+
nested_interval_scope_columns.each do |column_name|
|
95
|
+
conditions[column_name] = send(column_name)
|
96
|
+
end
|
97
|
+
self.class.where conditions
|
98
|
+
end
|
99
|
+
|
100
|
+
# Updates record, updating descendants if parent association updated,
|
101
|
+
# in which case caller should first acquire table lock.
|
102
|
+
def update_nested_interval
|
103
|
+
if read_attribute(nested_interval_foreign_key).nil?
|
104
|
+
set_nested_interval_for_top
|
105
|
+
elsif !association(:parent).updated?
|
106
|
+
db_self = self.class.find(id, :lock => true)
|
107
|
+
write_attribute(nested_interval_foreign_key, db_self.read_attribute(nested_interval_foreign_key))
|
108
|
+
set_nested_interval db_self.lftp, db_self.lftq
|
109
|
+
else # move
|
110
|
+
# No locking in this case -- caller should have acquired table lock.
|
111
|
+
update_nested_interval_move
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def update_nested_interval_move
|
116
|
+
db_self = self.class.find(id)
|
117
|
+
db_parent = self.class.find(read_attribute(nested_interval_foreign_key))
|
118
|
+
if db_self.ancestor_of?(db_parent)
|
119
|
+
errors.add nested_interval_foreign_key, "is descendant"
|
120
|
+
raise ActiveRecord::RecordInvalid, self
|
121
|
+
end
|
122
|
+
|
123
|
+
set_nested_interval *parent.next_child_lft
|
124
|
+
mysql_tmp = "@" if ["MySQL", "Mysql2"].include?(connection.adapter_name)
|
125
|
+
cpp = db_self.lftq * rgtp - db_self.rgtq * lftp
|
126
|
+
cpq = db_self.rgtp * lftp - db_self.lftp * rgtp
|
127
|
+
cqp = db_self.lftq * rgtq - db_self.rgtq * lftq
|
128
|
+
cqq = db_self.rgtp * lftq - db_self.lftp * rgtq
|
129
|
+
|
130
|
+
db_descendants = db_self.descendants
|
131
|
+
|
132
|
+
if has_attribute?(:rgtp) && has_attribute?(:rgtq)
|
133
|
+
db_descendants.update_all %(
|
134
|
+
rgtp = #{cpp} * rgtp + #{cpq} * rgtq,
|
135
|
+
rgtq = #{cqp} * #{mysql_tmp}rgtp + #{cqq} * rgtq
|
136
|
+
), mysql_tmp && %(@rgtp := rgtp)
|
137
|
+
db_descendants.update_all "rgt = 1.0 * rgtp / rgtq" if has_attribute?(:rgt)
|
138
|
+
end
|
139
|
+
|
140
|
+
db_descendants.update_all %(
|
141
|
+
lftp = #{cpp} * lftp + #{cpq} * lftq,
|
142
|
+
lftq = #{cqp} * #{mysql_tmp}lftp + #{cqq} * lftq
|
143
|
+
), mysql_tmp && %(@lftp := lftp)
|
144
|
+
|
145
|
+
db_descendants.update_all %(lft = 1.0 * lftp / lftq) if has_attribute?(:lft)
|
146
|
+
end
|
147
|
+
|
148
|
+
def ancestor_of?(node)
|
149
|
+
node.lftp == lftp && node.lftq == lftq ||
|
150
|
+
node.lftp > node.lftq * lftp / lftq &&
|
151
|
+
node.lftp <= node.lftq * rgtp / rgtq &&
|
152
|
+
(node.lftp != rgtp || node.lftq != rgtq)
|
153
|
+
end
|
154
|
+
|
155
|
+
def ancestors
|
156
|
+
sqls = ["NULL"]
|
157
|
+
p, q = lftp, lftq
|
158
|
+
while p != 0
|
159
|
+
x = p.inverse(q)
|
160
|
+
p, q = (x * p - 1) / q, x
|
161
|
+
sqls << "lftq = #{q} AND lftp = #{p}"
|
162
|
+
end
|
163
|
+
nested_interval_scope.where(sqls * ' OR ')
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns depth by counting ancestors up to 0 / 1.
|
167
|
+
def depth
|
168
|
+
n = 0
|
169
|
+
p, q = lftp, lftq
|
170
|
+
while p != 0
|
171
|
+
x = p.inverse(q)
|
172
|
+
p, q = (x * p - 1) / q, x
|
173
|
+
n += 1
|
174
|
+
end
|
175
|
+
n
|
176
|
+
end
|
177
|
+
|
178
|
+
def lft; 1.0 * lftp / lftq end
|
179
|
+
def rgt; 1.0 * rgtp / rgtq end
|
180
|
+
|
181
|
+
# Returns numerator of right end of interval.
|
182
|
+
def rgtp
|
183
|
+
case lftp
|
184
|
+
when 0 then 1
|
185
|
+
when 1 then 1
|
186
|
+
else lftq.inverse(lftp)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns denominator of right end of interval.
|
191
|
+
def rgtq
|
192
|
+
case lftp
|
193
|
+
when 0 then 1
|
194
|
+
when 1 then lftq - 1
|
195
|
+
else (lftq.inverse(lftp) * lftq - 1) / lftp
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns left end of interval for next child.
|
200
|
+
def next_child_lft
|
201
|
+
if child = children.order('lftq DESC').first
|
202
|
+
return lftp + child.lftp, lftq + child.lftq
|
203
|
+
else
|
204
|
+
return lftp + rgtp, lftq + rgtq
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns left end of interval for next root.
|
209
|
+
def next_root_lft
|
210
|
+
vr = self.class.new # a virtual root
|
211
|
+
vr.set_nested_interval 0, 1
|
212
|
+
if child = nested_interval_scope.roots.order('lftq DESC').first
|
213
|
+
return vr.lftp + child.lftp, vr.lftq + child.lftq
|
214
|
+
else
|
215
|
+
return vr.lftp + vr.rgtp, vr.lftq + vr.rgtq
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
end
|
@@ -4,292 +4,53 @@
|
|
4
4
|
# Copyright (c) 2012 Nicolae Claudius
|
5
5
|
# https://github.com/clyfe
|
6
6
|
|
7
|
-
require 'acts_as_nested_interval/version'
|
8
7
|
require 'acts_as_nested_interval/core_ext/integer'
|
8
|
+
require 'acts_as_nested_interval/version'
|
9
|
+
require 'acts_as_nested_interval/instance_methods'
|
10
|
+
require 'acts_as_nested_interval/class_methods'
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# or all ancestors with just one select query. You can insert and delete
|
15
|
-
# records without a full table update.
|
16
|
-
module ClassMethods
|
12
|
+
# This act implements a nested-interval tree. You can find all descendants
|
13
|
+
# or all ancestors with just one select query. You can insert and delete
|
14
|
+
# records without a full table update.
|
15
|
+
module ActsAsNestedInterval
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
before_update :update_nested_interval
|
54
|
-
|
55
|
-
if columns_hash["lft"]
|
56
|
-
def descendants
|
57
|
-
quoted_table_name = self.class.quoted_table_name
|
58
|
-
nested_interval_scope.where <<-SQL
|
59
|
-
#{lftp} < #{quoted_table_name}.lftp AND
|
60
|
-
#{quoted_table_name}.lft BETWEEN #{1.0 * lftp / lftq} AND #{1.0 * rgtp / rgtq}
|
61
|
-
SQL
|
62
|
-
end
|
63
|
-
elsif nested_interval_lft_index
|
64
|
-
def descendants
|
65
|
-
quoted_table_name = self.class.quoted_table_name
|
66
|
-
nested_interval_scope.where <<-SQL
|
67
|
-
#{lftp} < #{quoted_table_name}.lftp AND
|
68
|
-
1.0 * #{quoted_table_name}.lftp / #{quoted_table_name}.lftq BETWEEN
|
69
|
-
#{1.0 * lftp / lftq} AND
|
70
|
-
#{1.0 * rgtp / rgtq}
|
71
|
-
SQL
|
72
|
-
end
|
73
|
-
elsif connection.adapter_name == "MySQL"
|
74
|
-
def descendants
|
75
|
-
quoted_table_name = self.class.quoted_table_name
|
76
|
-
nested_interval_scope.where <<-SQL
|
77
|
-
( #{quoted_table_name}.lftp != #{rgtp} OR
|
78
|
-
#{quoted_table_name}.lftq != #{rgtq}
|
79
|
-
) AND
|
80
|
-
#{quoted_table_name}.lftp BETWEEN
|
81
|
-
1 + #{quoted_table_name}.lftq * #{lftp} DIV #{lftq} AND
|
82
|
-
#{quoted_table_name}.lftq * #{rgtp} DIV #{rgtq}
|
83
|
-
SQL
|
84
|
-
end
|
85
|
-
else
|
86
|
-
def descendants
|
87
|
-
quoted_table_name = self.class.quoted_table_name
|
88
|
-
nested_interval_scope.where <<-SQL
|
89
|
-
( #{quoted_table_name}.lftp != #{rgtp} OR
|
90
|
-
#{quoted_table_name}.lftq != #{rgtq}
|
91
|
-
) AND
|
92
|
-
#{quoted_table_name}.lftp BETWEEN
|
93
|
-
1 + #{quoted_table_name}.lftq * CAST(#{lftp} AS BIGINT) / #{lftq} AND
|
94
|
-
#{quoted_table_name}.lftq * CAST(#{rgtp} AS BIGINT) / #{rgtq}
|
95
|
-
SQL
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def self.rebuild_nested_interval_tree!
|
100
|
-
# temporary changes
|
101
|
-
skip_callback :update, :before, :update_nested_interval
|
102
|
-
skip_callback :update, :before, :sync_childre
|
103
|
-
old_default_scopes = default_scopes # save to revert later
|
104
|
-
default_scope where("#{quoted_table_name}.lftq > 0") # use lft1 > 0 as a "migrated?" flag
|
105
|
-
|
106
|
-
# zero all intervals
|
107
|
-
update_hash = {lftp: 0, lftq: 0}
|
108
|
-
update_hash[:rgtp] = 0 if columns_hash["rgtp"]
|
109
|
-
update_hash[:rgtq] = 0 if columns_hash["rgtq"]
|
110
|
-
update_hash[:lft] = 0 if columns_hash["lft"]
|
111
|
-
update_hash[:rgt] = 0 if columns_hash["rgt"]
|
112
|
-
update_all update_hash
|
113
|
-
|
114
|
-
# recompute intervals
|
115
|
-
clear_cache!
|
116
|
-
update_subtree = lambda { |node|
|
117
|
-
node.create_nested_interval
|
118
|
-
node.save
|
119
|
-
node.class.unscoped.where(nested_interval_foreign_key => node.id).find_each &update_subtree
|
120
|
-
}
|
121
|
-
unscoped.roots.find_each &update_subtree
|
122
|
-
|
123
|
-
# revert changes
|
124
|
-
set_callback :update, :before, :update_nested_interval
|
125
|
-
set_callback :update, :before, :sync_childre
|
126
|
-
self.default_scopes = old_default_scopes
|
127
|
-
end
|
128
|
-
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
17
|
+
# The +options+ hash can include:
|
18
|
+
# * <tt>:foreign_key</tt> -- the self-reference foreign key column name (default :parent_id).
|
19
|
+
# * <tt>:scope_columns</tt> -- an array of columns to scope independent trees.
|
20
|
+
# * <tt>:lft_index</tt> -- whether to use functional index for lft (default false).
|
21
|
+
# * <tt>:virtual_root</tt> -- whether to compute root's interval as in an upper root (default false)
|
22
|
+
def acts_as_nested_interval(options = {})
|
23
|
+
cattr_accessor :nested_interval_foreign_key
|
24
|
+
cattr_accessor :nested_interval_scope_columns
|
25
|
+
cattr_accessor :nested_interval_lft_index
|
26
|
+
|
27
|
+
cattr_accessor :virtual_root
|
28
|
+
self.virtual_root = !!options[:virtual_root]
|
29
|
+
|
30
|
+
self.nested_interval_foreign_key = options[:foreign_key] || :parent_id
|
31
|
+
self.nested_interval_scope_columns = Array(options[:scope_columns])
|
32
|
+
self.nested_interval_lft_index = options[:lft_index]
|
33
|
+
|
34
|
+
belongs_to :parent, class_name: name, foreign_key: nested_interval_foreign_key
|
35
|
+
has_many :children, class_name: name, foreign_key: nested_interval_foreign_key, dependent: :destroy
|
36
|
+
scope :roots, where(nested_interval_foreign_key => nil)
|
37
|
+
|
38
|
+
if columns_hash["rgt"]
|
39
|
+
scope :preorder, order('rgt DESC, lftp ASC')
|
40
|
+
elsif columns_hash["rgtp"] && columns_hash["rgtq"]
|
41
|
+
scope :preorder, order('1.0 * rgtp / rgtq DESC, lftp ASC')
|
42
|
+
else
|
43
|
+
scope :preorder, order('nested_interval_rgt(lftp, lftq) DESC, lftp ASC')
|
44
|
+
end
|
45
|
+
|
46
|
+
before_create :create_nested_interval
|
47
|
+
before_destroy :destroy_nested_interval
|
48
|
+
before_update :update_nested_interval
|
49
|
+
|
50
|
+
include ActsAsNestedInterval::InstanceMethods
|
51
|
+
extend ActsAsNestedInterval::ClassMethods
|
132
52
|
end
|
133
53
|
|
134
|
-
module NodeInstanceMethods
|
135
|
-
def set_nested_interval(lftp, lftq)
|
136
|
-
self.lftp, self.lftq = lftp, lftq
|
137
|
-
self.rgtp = rgtp if has_attribute?(:rgtp)
|
138
|
-
self.rgtq = rgtq if has_attribute?(:rgtq)
|
139
|
-
self.lft = lft if has_attribute?(:lft)
|
140
|
-
self.rgt = rgt if has_attribute?(:rgt)
|
141
|
-
end
|
142
|
-
|
143
|
-
def set_nested_interval_for_top
|
144
|
-
if self.class.virtual_root
|
145
|
-
set_nested_interval *next_root_lft
|
146
|
-
else
|
147
|
-
set_nested_interval 0, 1
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
# Creates record.
|
152
|
-
def create_nested_interval
|
153
|
-
if read_attribute(nested_interval_foreign_key).nil?
|
154
|
-
set_nested_interval_for_top
|
155
|
-
else
|
156
|
-
set_nested_interval *parent.lock!.next_child_lft
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# Destroys record.
|
161
|
-
def destroy_nested_interval
|
162
|
-
lock! rescue nil
|
163
|
-
end
|
164
|
-
|
165
|
-
def nested_interval_scope
|
166
|
-
conditions = {}
|
167
|
-
nested_interval_scope_columns.each do |column_name|
|
168
|
-
conditions[column_name] = send(column_name)
|
169
|
-
end
|
170
|
-
self.class.where conditions
|
171
|
-
end
|
172
|
-
|
173
|
-
# Updates record, updating descendants if parent association updated,
|
174
|
-
# in which case caller should first acquire table lock.
|
175
|
-
def update_nested_interval
|
176
|
-
if read_attribute(nested_interval_foreign_key).nil?
|
177
|
-
set_nested_interval_for_top
|
178
|
-
elsif !association(:parent).updated?
|
179
|
-
db_self = self.class.find(id, :lock => true)
|
180
|
-
write_attribute(nested_interval_foreign_key, db_self.read_attribute(nested_interval_foreign_key))
|
181
|
-
set_nested_interval db_self.lftp, db_self.lftq
|
182
|
-
else # move
|
183
|
-
# No locking in this case -- caller should have acquired table lock.
|
184
|
-
update_nested_interval_move
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def update_nested_interval_move
|
189
|
-
db_self = self.class.find(id)
|
190
|
-
db_parent = self.class.find(read_attribute(nested_interval_foreign_key))
|
191
|
-
if db_self.ancestor_of?(db_parent)
|
192
|
-
errors.add nested_interval_foreign_key, "is descendant"
|
193
|
-
raise ActiveRecord::RecordInvalid, self
|
194
|
-
end
|
195
|
-
|
196
|
-
set_nested_interval *parent.next_child_lft
|
197
|
-
mysql_tmp = "@" if ["MySQL", "Mysql2"].include?(connection.adapter_name)
|
198
|
-
cpp = db_self.lftq * rgtp - db_self.rgtq * lftp
|
199
|
-
cpq = db_self.rgtp * lftp - db_self.lftp * rgtp
|
200
|
-
cqp = db_self.lftq * rgtq - db_self.rgtq * lftq
|
201
|
-
cqq = db_self.rgtp * lftq - db_self.lftp * rgtq
|
202
|
-
|
203
|
-
db_descendants = db_self.descendants
|
204
|
-
|
205
|
-
if has_attribute?(:rgtp) && has_attribute?(:rgtq)
|
206
|
-
db_descendants.update_all %(
|
207
|
-
rgtp = #{cpp} * rgtp + #{cpq} * rgtq,
|
208
|
-
rgtq = #{cqp} * #{mysql_tmp}rgtp + #{cqq} * rgtq
|
209
|
-
), mysql_tmp && %(@rgtp := rgtp)
|
210
|
-
db_descendants.update_all "rgt = 1.0 * rgtp / rgtq" if has_attribute?(:rgt)
|
211
|
-
end
|
212
|
-
|
213
|
-
db_descendants.update_all %(
|
214
|
-
lftp = #{cpp} * lftp + #{cpq} * lftq,
|
215
|
-
lftq = #{cqp} * #{mysql_tmp}lftp + #{cqq} * lftq
|
216
|
-
), mysql_tmp && %(@lftp := lftp)
|
217
|
-
|
218
|
-
db_descendants.update_all %(lft = 1.0 * lftp / lftq) if has_attribute?(:lft)
|
219
|
-
end
|
220
|
-
|
221
|
-
def ancestor_of?(node)
|
222
|
-
node.lftp == lftp && node.lftq == lftq ||
|
223
|
-
node.lftp > node.lftq * lftp / lftq &&
|
224
|
-
node.lftp <= node.lftq * rgtp / rgtq &&
|
225
|
-
(node.lftp != rgtp || node.lftq != rgtq)
|
226
|
-
end
|
227
|
-
|
228
|
-
def ancestors
|
229
|
-
sqls = ["NULL"]
|
230
|
-
p, q = lftp, lftq
|
231
|
-
while p != 0
|
232
|
-
x = p.inverse(q)
|
233
|
-
p, q = (x * p - 1) / q, x
|
234
|
-
sqls << "lftq = #{q} AND lftp = #{p}"
|
235
|
-
end
|
236
|
-
nested_interval_scope.where(sqls * ' OR ')
|
237
|
-
end
|
238
|
-
|
239
|
-
# Returns depth by counting ancestors up to 0 / 1.
|
240
|
-
def depth
|
241
|
-
n = 0
|
242
|
-
p, q = lftp, lftq
|
243
|
-
while p != 0
|
244
|
-
x = p.inverse(q)
|
245
|
-
p, q = (x * p - 1) / q, x
|
246
|
-
n += 1
|
247
|
-
end
|
248
|
-
n
|
249
|
-
end
|
250
|
-
|
251
|
-
def lft; 1.0 * lftp / lftq end
|
252
|
-
def rgt; 1.0 * rgtp / rgtq end
|
253
|
-
|
254
|
-
# Returns numerator of right end of interval.
|
255
|
-
def rgtp
|
256
|
-
case lftp
|
257
|
-
when 0 then 1
|
258
|
-
when 1 then 1
|
259
|
-
else lftq.inverse(lftp)
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
# Returns denominator of right end of interval.
|
264
|
-
def rgtq
|
265
|
-
case lftp
|
266
|
-
when 0 then 1
|
267
|
-
when 1 then lftq - 1
|
268
|
-
else (lftq.inverse(lftp) * lftq - 1) / lftp
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
# Returns left end of interval for next child.
|
273
|
-
def next_child_lft
|
274
|
-
if child = children.order('lftq DESC').first
|
275
|
-
return lftp + child.lftp, lftq + child.lftq
|
276
|
-
else
|
277
|
-
return lftp + rgtp, lftq + rgtq
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
# Returns left end of interval for next root.
|
282
|
-
def next_root_lft
|
283
|
-
vr = self.class.new # a virtual root
|
284
|
-
vr.set_nested_interval 0, 1
|
285
|
-
if child = nested_interval_scope.roots.order('lftq DESC').first
|
286
|
-
return vr.lftp + child.lftp, vr.lftq + child.lftq
|
287
|
-
else
|
288
|
-
return vr.lftp + vr.rgtp, vr.lftq + vr.rgtq
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
292
54
|
end
|
293
55
|
|
294
|
-
ActiveRecord::Base.send :
|
295
|
-
|
56
|
+
ActiveRecord::Base.send :extend, ActsAsNestedInterval
|