closure_tree 3.7.3 → 3.8.0
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 +35 -0
- data/lib/closure_tree.rb +0 -1
- data/lib/closure_tree/acts_as_tree.rb +12 -564
- data/lib/closure_tree/columns.rb +102 -0
- data/lib/closure_tree/deterministic_ordering.rb +39 -0
- data/lib/closure_tree/model.rb +373 -0
- data/lib/closure_tree/numeric_deterministic_ordering.rb +65 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/closure_tree/with_advisory_lock.rb +18 -0
- data/spec/label_spec.rb +36 -3
- metadata +180 -182
data/README.md
CHANGED
@@ -20,9 +20,11 @@ closure_tree has some great features:
|
|
20
20
|
* 2 SQL INSERTs on node creation
|
21
21
|
* 3 SQL INSERT/UPDATEs on node reparenting
|
22
22
|
* Support for reparenting children (and all their progeny)
|
23
|
+
* Support for [concurrency](#concurrency) (using [with_advisory_lock](https://github/mceachen/with_advisory_lock))
|
23
24
|
* Support for polymorphism [STI](#sti) within the hierarchy
|
24
25
|
* ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
|
25
26
|
* Support for [deterministic ordering](#deterministic-ordering) of children
|
27
|
+
* Support for [preordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order) traversal of descendants
|
26
28
|
* Support for single-select depth-limited [nested hashes](#nested-hashes)
|
27
29
|
* Excellent [test coverage](#testing) in a variety of environments
|
28
30
|
|
@@ -37,6 +39,7 @@ for a description of different tree storage algorithms.
|
|
37
39
|
- [Accessing Data](#accessing-data)
|
38
40
|
- [Polymorphic hierarchies with STI](#polymorphic-hierarchies-with-sti)
|
39
41
|
- [Deterministic ordering](#deterministic-ordering)
|
42
|
+
- [Concurrency](#concurrency)
|
40
43
|
- [FAQ](#faq)
|
41
44
|
- [Testing](#testing)
|
42
45
|
- [Change log](#change-log)
|
@@ -317,6 +320,9 @@ When you enable ```order```, you'll also have the following new methods injected
|
|
317
320
|
|
318
321
|
If your ```order``` column is an integer attribute, you'll also have these:
|
319
322
|
|
323
|
+
* ```node1.self_and_descendants_preordered``` which will return descendants,
|
324
|
+
[pre-ordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order).
|
325
|
+
|
320
326
|
* ```node1.prepend_sibling(node2)``` which will
|
321
327
|
1. set ```node2``` to the same parent as ```node1```,
|
322
328
|
2. set ```node2```'s order column to 1 less than ```node1```'s value, and
|
@@ -353,6 +359,29 @@ root.reload.children.collect(&:name)
|
|
353
359
|
=> ["b", "c", "a"]
|
354
360
|
```
|
355
361
|
|
362
|
+
## Concurrency
|
363
|
+
|
364
|
+
Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.
|
365
|
+
```#find_or_create_by_path```, for example, may create duplicate nodes.
|
366
|
+
|
367
|
+
Database row-level locks work correctly with PostgreSQL, but MySQL's row-level locking is broken, and
|
368
|
+
erroneously reports deadlocks where there are none. To work around this, and have a consistent implementation
|
369
|
+
for both MySQL and PostgreSQL, [with_advisory_lock](https://github.com/mceachen/with_advisory_lock)
|
370
|
+
is used automatically to ensure correctness.
|
371
|
+
|
372
|
+
If you are already managing concurrency elsewhere in your application, and want to disable the use
|
373
|
+
of with_advisory_lock, pass ```:with_advisory_lock => false``` in the options hash:
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
class Tag
|
377
|
+
acts_as_tree :with_advisory_lock => false
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
Note that you *will eventually have data corruption* if you disable advisory locks, write to your
|
382
|
+
database with multiple threads, and don't provide an alternative mutex.
|
383
|
+
|
384
|
+
|
356
385
|
## FAQ
|
357
386
|
|
358
387
|
### Does this gem support multiple parents?
|
@@ -396,6 +425,12 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
|
|
396
425
|
|
397
426
|
## Change log
|
398
427
|
|
428
|
+
### 3.8.0
|
429
|
+
|
430
|
+
* Support for preordered descendants. This requires a numeric sort order column.
|
431
|
+
Resolves [feature request 38](https://github.com/mceachen/closure_tree/issues/38).
|
432
|
+
* Moved modules from ```acts_as_tree``` into separate files
|
433
|
+
|
399
434
|
### 3.7.3
|
400
435
|
|
401
436
|
Due to MySQL's inability to lock rows properly, I've switched to advisory_locks for
|
data/lib/closure_tree.rb
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
require 'closure_tree/columns'
|
2
|
+
require 'closure_tree/deterministic_ordering'
|
3
|
+
require 'closure_tree/model'
|
4
|
+
require 'closure_tree/numeric_deterministic_ordering'
|
5
|
+
require 'closure_tree/with_advisory_lock'
|
6
|
+
|
1
7
|
module ClosureTree
|
2
8
|
module ActsAsTree
|
3
9
|
def acts_as_tree(options = {})
|
@@ -8,7 +14,8 @@ module ClosureTree
|
|
8
14
|
:ct_base_class => self,
|
9
15
|
:parent_column_name => 'parent_id',
|
10
16
|
:dependent => :nullify, # or :destroy or :delete_all -- see the README
|
11
|
-
:name_column => 'name'
|
17
|
+
:name_column => 'name',
|
18
|
+
:with_advisory_lock => true
|
12
19
|
}.merge(options)
|
13
20
|
|
14
21
|
raise IllegalArgumentException, "name_column can't be 'path'" if closure_tree_options[:name_column] == 'path'
|
@@ -16,6 +23,9 @@ module ClosureTree
|
|
16
23
|
include ClosureTree::Columns
|
17
24
|
extend ClosureTree::Columns
|
18
25
|
|
26
|
+
include ClosureTree::WithAdvisoryLock
|
27
|
+
extend ClosureTree::WithAdvisoryLock
|
28
|
+
|
19
29
|
# Auto-inject the hierarchy table
|
20
30
|
# See https://github.com/patshaughnessy/class_factory/blob/master/lib/class_factory/class_factory.rb
|
21
31
|
class_attribute :hierarchy_class
|
@@ -34,573 +44,11 @@ module ClosureTree
|
|
34
44
|
|
35
45
|
self.hierarchy_class.table_name = hierarchy_table_name
|
36
46
|
|
47
|
+
include ClosureTree::Model
|
37
48
|
unless order_option.nil?
|
38
49
|
include ClosureTree::DeterministicOrdering
|
39
50
|
include ClosureTree::DeterministicNumericOrdering if order_is_numeric
|
40
51
|
end
|
41
|
-
|
42
|
-
include ClosureTree::Model
|
43
|
-
|
44
|
-
validate :ct_validate
|
45
|
-
before_save :ct_before_save
|
46
|
-
after_save :ct_after_save
|
47
|
-
before_destroy :ct_before_destroy
|
48
|
-
|
49
|
-
belongs_to :parent,
|
50
|
-
:class_name => ct_class.to_s,
|
51
|
-
:foreign_key => parent_column_name
|
52
|
-
|
53
|
-
unless defined?(ActiveModel::ForbiddenAttributesProtection) && ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
|
54
|
-
attr_accessible :parent
|
55
|
-
end
|
56
|
-
|
57
|
-
has_many :children, with_order_option(
|
58
|
-
:class_name => ct_class.to_s,
|
59
|
-
:foreign_key => parent_column_name,
|
60
|
-
:dependent => closure_tree_options[:dependent]
|
61
|
-
)
|
62
|
-
|
63
|
-
has_many :ancestor_hierarchies,
|
64
|
-
:class_name => hierarchy_class_name,
|
65
|
-
:foreign_key => "descendant_id",
|
66
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc",
|
67
|
-
:dependent => :destroy
|
68
|
-
|
69
|
-
has_many :self_and_ancestors,
|
70
|
-
:through => :ancestor_hierarchies,
|
71
|
-
:source => :ancestor,
|
72
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc"
|
73
|
-
|
74
|
-
has_many :descendant_hierarchies,
|
75
|
-
:class_name => hierarchy_class_name,
|
76
|
-
:foreign_key => "ancestor_id",
|
77
|
-
:order => "#{quoted_hierarchy_table_name}.generations asc",
|
78
|
-
:dependent => :destroy
|
79
|
-
# TODO: FIXME: this collection currently ignores sort_order
|
80
|
-
# (because the quoted_table_named would need to be joined in to get to the order column)
|
81
|
-
|
82
|
-
has_many :self_and_descendants,
|
83
|
-
:through => :descendant_hierarchies,
|
84
|
-
:source => :descendant,
|
85
|
-
:order => append_order("#{quoted_hierarchy_table_name}.generations asc")
|
86
|
-
|
87
|
-
def self.roots
|
88
|
-
where(parent_column_name => nil)
|
89
|
-
end
|
90
|
-
|
91
|
-
# There is no default depth limit. This might be crazy-big, depending
|
92
|
-
# on your tree shape. Hash huge trees at your own peril!
|
93
|
-
def self.hash_tree(options = {})
|
94
|
-
build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
95
|
-
end
|
96
|
-
|
97
|
-
def find_all_by_generation(generation_level)
|
98
|
-
s = joins(<<-SQL)
|
99
|
-
INNER JOIN (
|
100
|
-
SELECT #{primary_key} as root_id
|
101
|
-
FROM #{quoted_table_name}
|
102
|
-
WHERE #{quoted_parent_column_name} IS NULL
|
103
|
-
) AS roots ON (1 = 1)
|
104
|
-
INNER JOIN (
|
105
|
-
SELECT ancestor_id, descendant_id
|
106
|
-
FROM #{quoted_hierarchy_table_name}
|
107
|
-
GROUP BY 1, 2
|
108
|
-
HAVING MAX(generations) = #{generation_level.to_i}
|
109
|
-
) AS descendants ON (
|
110
|
-
#{quoted_table_name}.#{primary_key} = descendants.descendant_id
|
111
|
-
AND roots.root_id = descendants.ancestor_id
|
112
|
-
)
|
113
|
-
SQL
|
114
|
-
order_option ? s.order(order_option) : s
|
115
|
-
end
|
116
|
-
|
117
|
-
def self.leaves
|
118
|
-
s = joins(<<-SQL)
|
119
|
-
INNER JOIN (
|
120
|
-
SELECT ancestor_id
|
121
|
-
FROM #{quoted_hierarchy_table_name}
|
122
|
-
GROUP BY 1
|
123
|
-
HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0
|
124
|
-
) AS leaves ON (#{quoted_table_name}.#{primary_key} = leaves.ancestor_id)
|
125
|
-
SQL
|
126
|
-
order_option ? s.order(order_option) : s
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
module Model
|
132
|
-
extend ActiveSupport::Concern
|
133
|
-
|
134
|
-
# Returns true if this node has no parents.
|
135
|
-
def root?
|
136
|
-
ct_parent_id.nil?
|
137
|
-
end
|
138
|
-
|
139
|
-
# Returns true if this node has a parent, and is not a root.
|
140
|
-
def child?
|
141
|
-
!parent.nil?
|
142
|
-
end
|
143
|
-
|
144
|
-
# Returns true if this node has no children.
|
145
|
-
def leaf?
|
146
|
-
children.empty?
|
147
|
-
end
|
148
|
-
|
149
|
-
# Returns the farthest ancestor, or self if +root?+
|
150
|
-
def root
|
151
|
-
self_and_ancestors.where(parent_column_name.to_sym => nil).first
|
152
|
-
end
|
153
|
-
|
154
|
-
def leaves
|
155
|
-
self_and_descendants.leaves
|
156
|
-
end
|
157
|
-
|
158
|
-
def depth
|
159
|
-
ancestors.size
|
160
|
-
end
|
161
|
-
|
162
|
-
alias :level :depth
|
163
|
-
|
164
|
-
def ancestors
|
165
|
-
without_self(self_and_ancestors)
|
166
|
-
end
|
167
|
-
|
168
|
-
def ancestor_ids
|
169
|
-
ids_from(ancestors)
|
170
|
-
end
|
171
|
-
|
172
|
-
# Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
|
173
|
-
# to the +name_column+.
|
174
|
-
# (so child.ancestry_path == +%w{grandparent parent child}+
|
175
|
-
def ancestry_path(to_s_column = name_column)
|
176
|
-
self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
|
177
|
-
end
|
178
|
-
|
179
|
-
def descendants
|
180
|
-
without_self(self_and_descendants)
|
181
|
-
end
|
182
|
-
|
183
|
-
def descendant_ids
|
184
|
-
ids_from(descendants)
|
185
|
-
end
|
186
|
-
|
187
|
-
def self_and_siblings
|
188
|
-
s = ct_base_class.where(parent_column_sym => parent)
|
189
|
-
order_option.present? ? s.order(quoted_order_column) : s
|
190
|
-
end
|
191
|
-
|
192
|
-
def siblings
|
193
|
-
without_self(self_and_siblings)
|
194
|
-
end
|
195
|
-
|
196
|
-
def sibling_ids
|
197
|
-
ids_from(siblings)
|
198
|
-
end
|
199
|
-
|
200
|
-
# Alias for appending to the children collection.
|
201
|
-
# You can also add directly to the children collection, if you'd prefer.
|
202
|
-
def add_child(child_node)
|
203
|
-
children << child_node
|
204
|
-
child_node
|
205
|
-
end
|
206
|
-
|
207
|
-
# Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
|
208
|
-
def find_by_path(path)
|
209
|
-
path = path.is_a?(Enumerable) ? path.dup : [path]
|
210
|
-
node = self
|
211
|
-
while !path.empty? && node
|
212
|
-
node = node.children.where(name_sym => path.shift).first
|
213
|
-
end
|
214
|
-
node
|
215
|
-
end
|
216
|
-
|
217
|
-
# Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
|
218
|
-
def find_or_create_by_path(path, attributes = {})
|
219
|
-
with_advisory_lock("closure_tree") do
|
220
|
-
transaction do
|
221
|
-
subpath = path.is_a?(Enumerable) ? path.dup : [path]
|
222
|
-
child_name = subpath.shift
|
223
|
-
return self unless child_name
|
224
|
-
child = transaction do
|
225
|
-
attrs = {name_sym => child_name}
|
226
|
-
attrs[:type] = self.type if ct_subclass? && ct_has_type?
|
227
|
-
self.children.where(attrs).first || begin
|
228
|
-
child = self.class.new(attributes.merge(attrs))
|
229
|
-
self.children << child
|
230
|
-
child
|
231
|
-
end
|
232
|
-
end
|
233
|
-
child.find_or_create_by_path(subpath, attributes)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
def find_all_by_generation(generation_level)
|
239
|
-
s = ct_base_class.joins(<<-SQL)
|
240
|
-
INNER JOIN (
|
241
|
-
SELECT descendant_id
|
242
|
-
FROM #{quoted_hierarchy_table_name}
|
243
|
-
WHERE ancestor_id = #{ct_quote(self.id)}
|
244
|
-
GROUP BY 1
|
245
|
-
HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
|
246
|
-
) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
|
247
|
-
SQL
|
248
|
-
order_option ? s.order(order_option) : s
|
249
|
-
end
|
250
|
-
|
251
|
-
def hash_tree_scope(limit_depth = nil)
|
252
|
-
scope = self_and_descendants
|
253
|
-
if limit_depth
|
254
|
-
scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
|
255
|
-
else
|
256
|
-
scope
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def hash_tree(options = {})
|
261
|
-
self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
262
|
-
end
|
263
|
-
|
264
|
-
def ct_parent_id
|
265
|
-
read_attribute(parent_column_sym)
|
266
|
-
end
|
267
|
-
|
268
|
-
protected
|
269
|
-
|
270
|
-
def ct_validate
|
271
|
-
if changes[parent_column_name] &&
|
272
|
-
parent.present? &&
|
273
|
-
parent.self_and_ancestors.include?(self)
|
274
|
-
errors.add(parent_column_sym, "You cannot add an ancestor as a descendant")
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
def ct_before_save
|
279
|
-
@was_new_record = new_record?
|
280
|
-
true # don't cancel the save
|
281
|
-
end
|
282
|
-
|
283
|
-
def ct_after_save
|
284
|
-
rebuild! if changes[parent_column_name] || @was_new_record
|
285
|
-
@was_new_record = false # we aren't new anymore.
|
286
|
-
true # don't cancel anything.
|
287
|
-
end
|
288
|
-
|
289
|
-
def rebuild!
|
290
|
-
with_advisory_lock("closure_tree") do
|
291
|
-
transaction do
|
292
|
-
delete_hierarchy_references unless @was_new_record
|
293
|
-
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
|
294
|
-
unless root?
|
295
|
-
connection.execute <<-SQL
|
296
|
-
INSERT INTO #{quoted_hierarchy_table_name}
|
297
|
-
(ancestor_id, descendant_id, generations)
|
298
|
-
SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
|
299
|
-
FROM #{quoted_hierarchy_table_name} x
|
300
|
-
WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
|
301
|
-
SQL
|
302
|
-
end
|
303
|
-
children.each { |c| c.rebuild! }
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
def ct_before_destroy
|
309
|
-
delete_hierarchy_references
|
310
|
-
if closure_tree_options[:dependent] == :nullify
|
311
|
-
children.each { |c| c.rebuild! }
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
def delete_hierarchy_references
|
316
|
-
# The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
|
317
|
-
# It shouldn't affect performance of postgresql.
|
318
|
-
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
319
|
-
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
320
|
-
connection.execute <<-SQL
|
321
|
-
DELETE FROM #{quoted_hierarchy_table_name}
|
322
|
-
WHERE descendant_id IN (
|
323
|
-
SELECT DISTINCT descendant_id
|
324
|
-
FROM ( SELECT descendant_id
|
325
|
-
FROM #{quoted_hierarchy_table_name}
|
326
|
-
WHERE ancestor_id = #{ct_quote(id)}
|
327
|
-
) AS x )
|
328
|
-
OR descendant_id = #{ct_quote(id)}
|
329
|
-
SQL
|
330
|
-
end
|
331
|
-
|
332
|
-
def without_self(scope)
|
333
|
-
scope.where(["#{quoted_table_name}.#{ct_base_class.primary_key} != ?", self])
|
334
|
-
end
|
335
|
-
|
336
|
-
def ids_from(scope)
|
337
|
-
if scope.respond_to? :pluck
|
338
|
-
scope.pluck(:id)
|
339
|
-
else
|
340
|
-
scope.select(:id).collect(&:id)
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
def ct_quote(field)
|
345
|
-
self.class.connection.quote(field)
|
346
|
-
end
|
347
|
-
|
348
|
-
# TODO: _parent_id will be removed in the next major version
|
349
|
-
alias :_parent_id :ct_parent_id
|
350
|
-
|
351
|
-
module ClassMethods
|
352
|
-
|
353
|
-
# Returns an arbitrary node that has no parents.
|
354
|
-
def root
|
355
|
-
roots.first
|
356
|
-
end
|
357
|
-
|
358
|
-
# Rebuilds the hierarchy table based on the parent_id column in the database.
|
359
|
-
# Note that the hierarchy table will be truncated.
|
360
|
-
def rebuild!
|
361
|
-
with_advisory_lock("closure_tree") do
|
362
|
-
transaction do
|
363
|
-
hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
|
364
|
-
roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
|
365
|
-
end
|
366
|
-
end
|
367
|
-
nil
|
368
|
-
end
|
369
|
-
|
370
|
-
# Find the node whose +ancestry_path+ is +path+
|
371
|
-
def find_by_path(path)
|
372
|
-
subpath = path.dup
|
373
|
-
root = roots.where(name_sym => subpath.shift).first
|
374
|
-
root.find_by_path(subpath) if root
|
375
|
-
end
|
376
|
-
|
377
|
-
# Find or create nodes such that the +ancestry_path+ is +path+
|
378
|
-
def find_or_create_by_path(path, attributes = {})
|
379
|
-
subpath = path.dup
|
380
|
-
root_name = subpath.shift
|
381
|
-
with_advisory_lock("closure_tree") do
|
382
|
-
transaction do
|
383
|
-
# shenanigans because find_or_create can't infer we want the same class as this:
|
384
|
-
# Note that roots will already be constrained to this subclass (in the case of polymorphism):
|
385
|
-
root = roots.where(name_sym => root_name).first
|
386
|
-
root ||= create!(attributes.merge(name_sym => root_name))
|
387
|
-
root.find_or_create_by_path(subpath, attributes)
|
388
|
-
end
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
def hash_tree_scope(limit_depth = nil)
|
393
|
-
# Deepest generation, within limit, for each descendant
|
394
|
-
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
395
|
-
generation_depth = <<-SQL
|
396
|
-
INNER JOIN (
|
397
|
-
SELECT descendant_id, MAX(generations) as depth
|
398
|
-
FROM #{quoted_hierarchy_table_name}
|
399
|
-
GROUP BY descendant_id
|
400
|
-
#{"HAVING MAX(generations) <= #{limit_depth - 1}" if limit_depth}
|
401
|
-
) AS generation_depth
|
402
|
-
ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
403
|
-
SQL
|
404
|
-
scoped.joins(generation_depth).order(append_order("generation_depth.depth"))
|
405
|
-
end
|
406
|
-
|
407
|
-
# Builds nested hash structure using the scope returned from the passed in scope
|
408
|
-
def build_hash_tree(tree_scope)
|
409
|
-
tree = ActiveSupport::OrderedHash.new
|
410
|
-
id_to_hash = {}
|
411
|
-
|
412
|
-
tree_scope.each do |ea|
|
413
|
-
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
|
414
|
-
if ea.root? || tree.empty? # We're at the top of the tree.
|
415
|
-
tree[ea] = h
|
416
|
-
else
|
417
|
-
id_to_hash[ea.ct_parent_id][ea] = h
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
tree
|
422
|
-
end
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# Mixed into both classes and instances to provide easy access to the column names
|
427
|
-
module Columns
|
428
|
-
|
429
|
-
def parent_column_name
|
430
|
-
closure_tree_options[:parent_column_name]
|
431
|
-
end
|
432
|
-
|
433
|
-
def parent_column_sym
|
434
|
-
parent_column_name.to_sym
|
435
|
-
end
|
436
|
-
|
437
|
-
def has_name?
|
438
|
-
ct_class.new.attributes.include? closure_tree_options[:name_column]
|
439
|
-
end
|
440
|
-
|
441
|
-
def name_column
|
442
|
-
closure_tree_options[:name_column]
|
443
|
-
end
|
444
|
-
|
445
|
-
def name_sym
|
446
|
-
name_column.to_sym
|
447
|
-
end
|
448
|
-
|
449
|
-
def hierarchy_table_name
|
450
|
-
# We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
|
451
|
-
# because they may have overridden the table name, which is what we want to be consistent with
|
452
|
-
# in order for the schema to make sense.
|
453
|
-
tablename = closure_tree_options[:hierarchy_table_name] ||
|
454
|
-
remove_prefix_and_suffix(ct_table_name).singularize + "_hierarchies"
|
455
|
-
|
456
|
-
ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
|
457
|
-
end
|
458
|
-
|
459
|
-
def hierarchy_class_name
|
460
|
-
closure_tree_options[:hierarchy_class_name] || ct_class.to_s + "Hierarchy"
|
461
|
-
end
|
462
|
-
|
463
|
-
def quoted_hierarchy_table_name
|
464
|
-
connection.quote_table_name hierarchy_table_name
|
465
|
-
end
|
466
|
-
|
467
|
-
def quoted_parent_column_name
|
468
|
-
connection.quote_column_name parent_column_name
|
469
|
-
end
|
470
|
-
|
471
|
-
def order_option
|
472
|
-
closure_tree_options[:order]
|
473
|
-
end
|
474
|
-
|
475
|
-
def with_order_option(options)
|
476
|
-
order_option ? options.merge(:order => order_option) : options
|
477
|
-
end
|
478
|
-
|
479
|
-
def append_order(order_by)
|
480
|
-
order_option ? "#{order_by}, #{order_option}" : order_by
|
481
|
-
end
|
482
|
-
|
483
|
-
def order_is_numeric
|
484
|
-
# The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
|
485
|
-
return false if order_option.nil? || !self.table_exists?
|
486
|
-
c = ct_class.columns_hash[order_option]
|
487
|
-
c && c.type == :integer
|
488
|
-
end
|
489
|
-
|
490
|
-
def ct_class
|
491
|
-
(self.is_a?(Class) ? self : self.class)
|
492
|
-
end
|
493
|
-
|
494
|
-
# This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
|
495
|
-
def ct_base_class
|
496
|
-
ct_class.closure_tree_options[:ct_base_class]
|
497
|
-
end
|
498
|
-
|
499
|
-
def ct_subclass?
|
500
|
-
ct_class != ct_class.base_class
|
501
|
-
end
|
502
|
-
|
503
|
-
def ct_attribute_names
|
504
|
-
@ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
|
505
|
-
end
|
506
|
-
|
507
|
-
def ct_has_type?
|
508
|
-
ct_attribute_names.include? 'type'
|
509
|
-
end
|
510
|
-
|
511
|
-
def ct_table_name
|
512
|
-
ct_class.table_name
|
513
|
-
end
|
514
|
-
|
515
|
-
def quoted_table_name
|
516
|
-
connection.quote_table_name ct_table_name
|
517
|
-
end
|
518
|
-
|
519
|
-
def remove_prefix_and_suffix(table_name)
|
520
|
-
prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
|
521
|
-
suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
|
522
|
-
table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
|
523
|
-
end
|
524
|
-
end
|
525
|
-
|
526
|
-
module DeterministicOrdering
|
527
|
-
def order_column
|
528
|
-
o = order_option
|
529
|
-
o.split(' ', 2).first if o
|
530
|
-
end
|
531
|
-
|
532
|
-
def require_order_column
|
533
|
-
raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
|
534
|
-
end
|
535
|
-
|
536
|
-
def order_column_sym
|
537
|
-
require_order_column
|
538
|
-
order_column.to_sym
|
539
|
-
end
|
540
|
-
|
541
|
-
def order_value
|
542
|
-
read_attribute(order_column_sym)
|
543
|
-
end
|
544
|
-
|
545
|
-
def order_value=(new_order_value)
|
546
|
-
write_attribute(order_column_sym, new_order_value)
|
547
|
-
end
|
548
|
-
|
549
|
-
def quoted_order_column(include_table_name = true)
|
550
|
-
require_order_column
|
551
|
-
prefix = include_table_name ? "#{quoted_table_name}." : ""
|
552
|
-
"#{prefix}#{connection.quote_column_name(order_column)}"
|
553
|
-
end
|
554
|
-
|
555
|
-
def siblings_before
|
556
|
-
siblings.where(["#{quoted_order_column} < ?", order_value])
|
557
|
-
end
|
558
|
-
|
559
|
-
def siblings_after
|
560
|
-
siblings.where(["#{quoted_order_column} > ?", order_value])
|
561
|
-
end
|
562
|
-
end
|
563
|
-
|
564
|
-
# This module is only included if the order column is an integer.
|
565
|
-
module DeterministicNumericOrdering
|
566
|
-
def append_sibling(sibling_node, use_update_all = true)
|
567
|
-
add_sibling(sibling_node, use_update_all, true)
|
568
|
-
end
|
569
|
-
|
570
|
-
def prepend_sibling(sibling_node, use_update_all = true)
|
571
|
-
add_sibling(sibling_node, use_update_all, false)
|
572
|
-
end
|
573
|
-
|
574
|
-
def add_sibling(sibling_node, use_update_all = true, add_after = true)
|
575
|
-
fail "can't add self as sibling" if self == sibling_node
|
576
|
-
# issue 40: we need to lock the parent to prevent deadlocks on parallel sibling additions
|
577
|
-
with_advisory_lock("closure_tree") do
|
578
|
-
transaction do
|
579
|
-
# issue 18: we need to set the order_value explicitly so subsequent orders will work.
|
580
|
-
update_attribute(:order_value, 0) if self.order_value.nil?
|
581
|
-
sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
|
582
|
-
# We need to incr the before_siblings to make room for sibling_node:
|
583
|
-
if use_update_all
|
584
|
-
col = quoted_order_column(false)
|
585
|
-
# issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
|
586
|
-
ct_base_class.update_all(
|
587
|
-
["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
|
588
|
-
["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
589
|
-
ct_parent_id,
|
590
|
-
sibling_node.order_value])
|
591
|
-
else
|
592
|
-
last_value = sibling_node.order_value.to_i
|
593
|
-
(add_after ? siblings_after : siblings_before.reverse).each do |ea|
|
594
|
-
last_value += (add_after ? 1 : -1)
|
595
|
-
ea.order_value = last_value
|
596
|
-
ea.save!
|
597
|
-
end
|
598
|
-
end
|
599
|
-
sibling_node.parent = self.parent
|
600
|
-
sibling_node.save!
|
601
|
-
sibling_node.reload
|
602
|
-
end
|
603
|
-
end
|
604
52
|
end
|
605
53
|
end
|
606
54
|
end
|