closure_tree 3.7.3 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,7 +2,6 @@ require 'active_support'
2
2
  require 'active_record'
3
3
 
4
4
  ActiveSupport.on_load :active_record do
5
- require 'with_advisory_lock'
6
5
  require 'closure_tree/acts_as_tree'
7
6
 
8
7
  ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
@@ -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