acts_as_nested_interval 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|