acts_as_ordered_tree 1.3.1 → 2.0.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/acts_as_ordered_tree.rb +22 -100
- data/lib/acts_as_ordered_tree/adapters.rb +17 -0
- data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
- data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
- data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
- data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
- data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
- data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
- data/lib/acts_as_ordered_tree/hooks.rb +38 -0
- data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
- data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
- data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
- data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
- data/lib/acts_as_ordered_tree/node.rb +78 -0
- data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
- data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
- data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
- data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
- data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
- data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
- data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
- data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
- data/lib/acts_as_ordered_tree/position.rb +143 -0
- data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
- data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
- data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
- data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
- data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
- data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
- data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
- data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
- data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
- data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
- data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
- data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
- data/lib/acts_as_ordered_tree/tree.rb +148 -0
- data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
- data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
- data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
- data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
- data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
- data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
- data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
- data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
- data/lib/acts_as_ordered_tree/validators.rb +1 -1
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +80 -909
- data/spec/adapters/postgresql_spec.rb +14 -0
- data/spec/adapters/recursive_spec.rb +12 -0
- data/spec/adapters/shared.rb +272 -0
- data/spec/callbacks_spec.rb +177 -0
- data/spec/counter_cache_spec.rb +31 -0
- data/spec/create_spec.rb +110 -0
- data/spec/destroy_spec.rb +57 -0
- data/spec/inheritance_spec.rb +176 -0
- data/spec/move_spec.rb +94 -0
- data/spec/node/movements/concurrent_movements_spec.rb +354 -0
- data/spec/node/movements/move_higher_spec.rb +46 -0
- data/spec/node/movements/move_lower_spec.rb +46 -0
- data/spec/node/movements/move_to_child_of_spec.rb +147 -0
- data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
- data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
- data/spec/node/movements/move_to_left_of_spec.rb +120 -0
- data/spec/node/movements/move_to_right_of_spec.rb +120 -0
- data/spec/node/movements/move_to_root_spec.rb +67 -0
- data/spec/node/predicates_spec.rb +211 -0
- data/spec/node/reloading_spec.rb +42 -0
- data/spec/node/siblings_spec.rb +193 -0
- data/spec/node/traversals_spec.rb +71 -0
- data/spec/persevering_transaction_spec.rb +98 -0
- data/spec/relation/arrangeable_spec.rb +88 -0
- data/spec/relation/iterable_spec.rb +104 -0
- data/spec/relation/preloaded_spec.rb +57 -0
- data/spec/reorder_spec.rb +83 -0
- data/spec/spec_helper.rb +30 -38
- data/spec/support/db/boot.rb +22 -0
- data/spec/{db → support/db}/config.travis.yml +2 -0
- data/spec/{db → support/db}/config.yml +1 -0
- data/spec/{db → support/db}/schema.rb +9 -0
- data/spec/support/factories.rb +2 -2
- data/spec/support/matchers.rb +67 -58
- data/spec/support/models.rb +6 -14
- data/spec/support/tree_factory.rb +315 -0
- data/spec/tree/children_association_spec.rb +72 -0
- data/spec/tree/columns_spec.rb +65 -0
- data/spec/tree/scopes_spec.rb +39 -0
- metadata +161 -43
- data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
- data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
- data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
- data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
- data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
- data/spec/concurrency_support_spec.rb +0 -156
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/iterators/arranger'
|
4
|
+
require 'acts_as_ordered_tree/iterators/orphans_pruner'
|
5
|
+
|
6
|
+
module ActsAsOrderedTree
|
7
|
+
module Relation
|
8
|
+
# This AR::Relation extension allows to arrange collection into
|
9
|
+
# Hash of nested Hashes
|
10
|
+
module Arrangeable
|
11
|
+
# Arrange associated collection into a nested hash of the form
|
12
|
+
# {node => children}, where children = {} if the node has no children.
|
13
|
+
#
|
14
|
+
# It is possible to discard orphaned nodes (nodes which don't have
|
15
|
+
# corresponding parent node in this collection) by passing `:orphans => :discard`
|
16
|
+
# as option.
|
17
|
+
#
|
18
|
+
# @param [Hash] options
|
19
|
+
# @option options [:discard, nil] :orphans
|
20
|
+
# @return [Hash<ActiveRecord::Base => Hash>]
|
21
|
+
def arrange(options = {})
|
22
|
+
collection = self
|
23
|
+
|
24
|
+
if options && options[:orphans] == :discard
|
25
|
+
collection = Iterators::OrphansPruner.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
@arranger ||= Iterators::Arranger.new(collection)
|
29
|
+
@arranger.arrange
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/iterators/level_calculator'
|
4
|
+
require 'acts_as_ordered_tree/iterators/orphans_pruner'
|
5
|
+
|
6
|
+
module ActsAsOrderedTree
|
7
|
+
module Relation
|
8
|
+
module Iterable
|
9
|
+
# Iterates over tree elements and determines the current level in the tree.
|
10
|
+
# Only accepts default ordering, no orphans allowed (they considered as root elements).
|
11
|
+
# This method is efficient on trees that don't cache level.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# node.descendants.each_with_level do |descendant, level|
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @return [Enumerator] if block is not given
|
18
|
+
def each_with_level(&block)
|
19
|
+
Iterators::LevelCalculator.new(self).each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Iterates over tree elements but discards any orphaned nodes (e.g. nodes
|
23
|
+
# which have a parent, but parent isn't in current collection).
|
24
|
+
#
|
25
|
+
# @example Collection with orphaned nodes
|
26
|
+
# # Assume we have following tree:
|
27
|
+
# # root 1
|
28
|
+
# # child 1
|
29
|
+
# # root 2
|
30
|
+
# # child 2
|
31
|
+
#
|
32
|
+
# MyModel.where('id != ?', root_1.id).extending(Iterable).each_without_orphans.to_a
|
33
|
+
# # => [root_2, child_2]
|
34
|
+
#
|
35
|
+
# @return [Enumerator] if block is not given
|
36
|
+
def each_without_orphans(&block)
|
37
|
+
Iterators::OrphansPruner.new(self).each(&block)
|
38
|
+
end
|
39
|
+
end # module Iterable
|
40
|
+
end # module Relation
|
41
|
+
end # module ActsAsOrderedTree
|
@@ -1,16 +1,51 @@
|
|
1
|
-
|
1
|
+
# coding: utf-8
|
2
2
|
|
3
3
|
module ActsAsOrderedTree
|
4
4
|
module Relation
|
5
|
-
#
|
6
|
-
|
7
|
-
|
5
|
+
# AR::Relation extension which adds ability to explicitly set records
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# records = MyModel.where(:parent_id => nil).to_a
|
9
|
+
# relation = MyModel.where(:parent_id => nil).
|
10
|
+
# extending(ActsAsOrderedTree::Relation::Preloaded).
|
11
|
+
# records(records)
|
12
|
+
# relation.to_a.should be records
|
13
|
+
module Preloaded
|
8
14
|
def records(records)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
15
|
+
@loaded = false
|
16
|
+
@records = records
|
17
|
+
|
18
|
+
build_where!
|
19
|
+
|
20
|
+
@loaded = true
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Reverse the existing order of records on the relation.
|
26
|
+
def reverse_order
|
27
|
+
(respond_to?(:spawn) ? spawn : clone).records(@records.reverse)
|
28
|
+
end
|
29
|
+
|
30
|
+
def reverse_order!
|
31
|
+
@records = @records.reverse
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Extending relation is not really intrusive operation, so we can save preloaded records
|
37
|
+
def extending(*)
|
38
|
+
super.tap { |relation| relation.records(@records) if loaded? }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def record_ids
|
43
|
+
@records.map { |r| r.id if r }.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_where!
|
47
|
+
self.where_values = build_where(:id => record_ids)
|
13
48
|
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
49
|
+
end # module Preloaded
|
50
|
+
end # module Relation
|
51
|
+
end # module ActsAsOrderedTree
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/persevering_transaction'
|
4
|
+
require 'acts_as_ordered_tree/transaction/callbacks'
|
5
|
+
|
6
|
+
module ActsAsOrderedTree
|
7
|
+
module Transaction
|
8
|
+
# Persevering transaction, which restarts on deadlock
|
9
|
+
#
|
10
|
+
# Here we have a tree of possible transaction types:
|
11
|
+
#
|
12
|
+
# Base (abstract)
|
13
|
+
# Save (abstract)
|
14
|
+
# Create
|
15
|
+
# Update (abstract)
|
16
|
+
# Move
|
17
|
+
# Reorder
|
18
|
+
# Destroy
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
class Base
|
22
|
+
extend Callbacks
|
23
|
+
|
24
|
+
attr_reader :node
|
25
|
+
|
26
|
+
delegate :record, :tree, :to => :node
|
27
|
+
delegate :connection, :to => :klass
|
28
|
+
|
29
|
+
# @param [ActsAsOrderedTree::Node] node
|
30
|
+
def initialize(node)
|
31
|
+
@node = node
|
32
|
+
end
|
33
|
+
|
34
|
+
# Start persevering transaction, which will restart on deadlock
|
35
|
+
def start(&block)
|
36
|
+
transaction.start do
|
37
|
+
run_callbacks(:transaction, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def klass
|
43
|
+
record.class
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns underlying transaction object
|
47
|
+
def transaction
|
48
|
+
@transaction ||= PerseveringTransaction.new(connection)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Trigger tree callback (before_add, after_add, before_remove, after_remove)
|
52
|
+
def trigger_callback(kind, owner)
|
53
|
+
tree.callbacks.send(kind, owner, record) if owner.present?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_support/callbacks'
|
2
|
+
|
3
|
+
module ActsAsOrderedTree
|
4
|
+
module Transaction
|
5
|
+
module Callbacks
|
6
|
+
def self.extended(base)
|
7
|
+
base.send(:include, ActiveSupport::Callbacks)
|
8
|
+
base.define_callbacks :transaction
|
9
|
+
end
|
10
|
+
|
11
|
+
def before(filter, *options, &block)
|
12
|
+
set_callback :transaction, :before, filter, *options, &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def after(filter, *options, &block)
|
16
|
+
set_callback :transaction, :after, filter, *options, &block
|
17
|
+
end
|
18
|
+
|
19
|
+
def around(filter, *options, &block)
|
20
|
+
set_callback :transaction, :around, filter, *options, &block
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method should be called in concrete transaction classes to prevent
|
24
|
+
# race conditions in multi-threaded environments.
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
def finalize
|
28
|
+
finalize_callbacks :transaction
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
Compatibility.version '< 3.2.0' do
|
33
|
+
def finalize_callbacks(kind)
|
34
|
+
__define_runner(kind)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Compatibility.version '>= 3.2.0', '< 4.0.0' do
|
39
|
+
def finalize_callbacks(kind)
|
40
|
+
__reset_runner(kind)
|
41
|
+
|
42
|
+
object = allocate
|
43
|
+
|
44
|
+
name = __callback_runner_name(nil, kind)
|
45
|
+
unless object.respond_to?(name, true)
|
46
|
+
str = object.send("_#{kind}_callbacks").compile(nil, object)
|
47
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
48
|
+
def #{name}() #{str} end
|
49
|
+
protected :#{name}
|
50
|
+
RUBY_EVAL
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Compatibility.version '>= 4.0.0', '< 4.1.0' do
|
56
|
+
def finalize_callbacks(kind)
|
57
|
+
__define_callbacks(kind, allocate)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Rails 4.1 is thread safe
|
62
|
+
Compatibility.version '>= 4.1.0' do
|
63
|
+
def finalize_callbacks(kind) end
|
64
|
+
end
|
65
|
+
end # module Callbacks
|
66
|
+
end # module Transaction
|
67
|
+
end # module ActsAsOrderedTree
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/transaction/save'
|
4
|
+
require 'acts_as_ordered_tree/transaction/dsl'
|
5
|
+
|
6
|
+
module ActsAsOrderedTree
|
7
|
+
module Transaction
|
8
|
+
# Create transaction (for new records only)
|
9
|
+
# @api private
|
10
|
+
class Create < Save
|
11
|
+
include DSL
|
12
|
+
|
13
|
+
before :push_to_bottom_after_commit, :if => 'push_to_bottom? && to.root?'
|
14
|
+
before :set_counter_cache, :if => 'tree.columns.counter_cache?'
|
15
|
+
before :increment_lower_positions, :unless => :push_to_bottom?
|
16
|
+
before 'trigger_callback(:before_add, to.parent)'
|
17
|
+
|
18
|
+
after 'to.increment_counter'
|
19
|
+
after 'node.reload'
|
20
|
+
after 'trigger_callback(:after_add, to.parent)'
|
21
|
+
|
22
|
+
finalize
|
23
|
+
|
24
|
+
private
|
25
|
+
def set_counter_cache
|
26
|
+
record[tree.columns.counter_cache] = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def increment_lower_positions
|
30
|
+
to.lower.update_all set position => position + 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# If record was created as root there is a chance that position will collide,
|
34
|
+
# but this callback will force record to placed at the bottom of tree.
|
35
|
+
#
|
36
|
+
# Yep, concurrency is a tough thing.
|
37
|
+
#
|
38
|
+
# @see https://github.com/take-five/acts_as_ordered_tree/issues/24
|
39
|
+
def push_to_bottom_after_commit
|
40
|
+
transaction.after_commit do
|
41
|
+
connection.logger.debug { "Forcing new record (id=#{record.id}, position=#{node.position}) to be placed to bottom" }
|
42
|
+
|
43
|
+
connection.transaction do
|
44
|
+
# lock new siblings
|
45
|
+
to.siblings.lock.reload
|
46
|
+
|
47
|
+
if positions_collided?
|
48
|
+
update_created set position => siblings.select(coalesce(max(position), 0) + 1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks if there is +position_column+ collision within new parent
|
55
|
+
def positions_collided?
|
56
|
+
to.siblings.where(id.not_eq(record.id).and(position.eq(to.position))).exists?
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_created(*args)
|
60
|
+
to.siblings.where(id.eq(record.id)).update_all(*args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def siblings
|
64
|
+
to.siblings.where(id.not_eq(record.id))
|
65
|
+
end
|
66
|
+
end # class Create
|
67
|
+
end # module Transaction
|
68
|
+
end # module ActsAsOrderedTree
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'acts_as_ordered_tree/transaction/base'
|
4
|
+
require 'acts_as_ordered_tree/transaction/dsl'
|
5
|
+
|
6
|
+
module ActsAsOrderedTree
|
7
|
+
module Transaction
|
8
|
+
class Destroy < Base
|
9
|
+
include DSL
|
10
|
+
|
11
|
+
attr_reader :from
|
12
|
+
|
13
|
+
before 'trigger_callback(:before_remove, from.parent)'
|
14
|
+
|
15
|
+
after :decrement_lower_positions
|
16
|
+
after 'from.decrement_counter'
|
17
|
+
after 'trigger_callback(:after_remove, from.parent)'
|
18
|
+
|
19
|
+
finalize
|
20
|
+
|
21
|
+
# @param [ActsAsOrderedTree::Node] node
|
22
|
+
# @param [ActsAsOrderedTree::Position] from from which position given +node+ is destroyed
|
23
|
+
def initialize(node, from)
|
24
|
+
super(node)
|
25
|
+
@from = from
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def decrement_lower_positions
|
30
|
+
from.lower.update_all set position => position - 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
|
5
|
+
module Arel
|
6
|
+
module Nodes
|
7
|
+
# Case node
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# switch.when(table[:x].gt(1), table[:y]).else(table[:z])
|
11
|
+
# # CASE WHEN "table"."x" > 1 THEN "table"."y" ELSE "table"."z" END
|
12
|
+
# switch.when(table[:x].gt(1)).then(table[:y]).else(table[:z])
|
13
|
+
class Case < Arel::Nodes::Node
|
14
|
+
include Arel::OrderPredications
|
15
|
+
include Arel::Predications
|
16
|
+
|
17
|
+
attr_reader :conditions, :default
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@conditions = []
|
21
|
+
@default = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def when(condition, expression = nil)
|
25
|
+
@conditions << When.new(condition, expression)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def then(expression)
|
30
|
+
@conditions.last.right = expression
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def else(expression)
|
35
|
+
@default = Else.new(expression)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class When < Arel::Nodes::Binary
|
41
|
+
end
|
42
|
+
|
43
|
+
class Else < Arel::Nodes::Unary
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Visitors
|
48
|
+
class ToSql < Arel::Visitors::ToSql.superclass
|
49
|
+
private
|
50
|
+
def visit_Arel_Nodes_Case o, *a
|
51
|
+
conditions = o.conditions.map { |x| visit x, *a }.join(' ')
|
52
|
+
default = o.default && visit(o.default, *a)
|
53
|
+
|
54
|
+
"CASE #{[conditions, default].compact.join(' ')} END"
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_Arel_Nodes_When o, *a
|
58
|
+
"WHEN #{visit o.left, *a} THEN #{visit o.right, *a}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_Arel_Nodes_Else o, *a
|
62
|
+
"ELSE #{visit o.expr, *a}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if Arel::VERSION >= '6.0.0'
|
66
|
+
def visit_Arel_Nodes_Case o, collector
|
67
|
+
collector << 'CASE '
|
68
|
+
o.conditions.each do |x|
|
69
|
+
visit x, collector
|
70
|
+
collector << ' '
|
71
|
+
end
|
72
|
+
if o.default
|
73
|
+
visit o.default, collector
|
74
|
+
collector << ' '
|
75
|
+
end
|
76
|
+
collector << 'END'
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_Arel_Nodes_When o, collector
|
80
|
+
collector << 'WHEN '
|
81
|
+
visit o.left, collector
|
82
|
+
collector << ' THEN '
|
83
|
+
visit o.right, collector
|
84
|
+
end
|
85
|
+
|
86
|
+
def visit_Arel_Nodes_Else o, collector
|
87
|
+
collector << 'ELSE'
|
88
|
+
visit o.expr, collector
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_NilClass o, collector
|
92
|
+
collector << 'NULL'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class DepthFirst < Arel::Visitors::Visitor
|
98
|
+
def visit_Arel_Nodes_Case o, *a
|
99
|
+
visit o.conditions, *a
|
100
|
+
visit o.default, *a
|
101
|
+
end
|
102
|
+
alias :visit_Arel_Nodes_When :binary
|
103
|
+
alias :visit_Arel_Nodes_Else :unary
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module ActsAsOrderedTree
|
109
|
+
module Transaction
|
110
|
+
# Simple DSL to generate complex UPDATE queries.
|
111
|
+
# Requires +record+ method.
|
112
|
+
#
|
113
|
+
# @api private
|
114
|
+
module DSL
|
115
|
+
module Shortcuts
|
116
|
+
INFIX_OPERATIONS = Hash[
|
117
|
+
:== => Arel::Nodes::Equality,
|
118
|
+
:'!=' => Arel::Nodes::NotEqual,
|
119
|
+
:> => Arel::Nodes::GreaterThan,
|
120
|
+
:>= => Arel::Nodes::GreaterThanOrEqual,
|
121
|
+
:< => Arel::Nodes::LessThan,
|
122
|
+
:<= => Arel::Nodes::LessThanOrEqual,
|
123
|
+
:=~ => Arel::Nodes::Matches,
|
124
|
+
:'!~' => Arel::Nodes::DoesNotMatch,
|
125
|
+
:| => Arel::Nodes::Or
|
126
|
+
]
|
127
|
+
|
128
|
+
# generate subclasses and methods
|
129
|
+
INFIX_OPERATIONS.each do |operator, klass|
|
130
|
+
subclass = Class.new(klass) { include Shortcuts }
|
131
|
+
const_set(klass.name.demodulize, subclass)
|
132
|
+
INFIX_OPERATIONS[operator] = subclass
|
133
|
+
|
134
|
+
define_method(operator) do |arg|
|
135
|
+
subclass.new(self, arg)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
And = Class.new(Arel::Nodes::And) { include Shortcuts }
|
140
|
+
|
141
|
+
def &(arg)
|
142
|
+
And.new [self, arg]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
Attribute = Class.new(Arel::Attributes::Attribute) { include Shortcuts }
|
147
|
+
SqlLiteral = Class.new(Arel::Nodes::SqlLiteral) { include Shortcuts }
|
148
|
+
|
149
|
+
NamedFunction = Class.new(Arel::Nodes::NamedFunction) {
|
150
|
+
include Shortcuts
|
151
|
+
include Arel::Math
|
152
|
+
}
|
153
|
+
|
154
|
+
# Create Arel::Nodes::Case node
|
155
|
+
def switch
|
156
|
+
Arel::Nodes::Case.new
|
157
|
+
end
|
158
|
+
|
159
|
+
# Create assignments expression for UPDATE statement
|
160
|
+
#
|
161
|
+
# @example
|
162
|
+
# Model.where(:parent_id => nil).update_all(set :name => switch.when(x < 10).then('OK').else('TOO LARGE'))
|
163
|
+
#
|
164
|
+
# @param [Hash] assignments
|
165
|
+
def set(assignments)
|
166
|
+
assignments.map do |attr, value|
|
167
|
+
next unless attr.present?
|
168
|
+
|
169
|
+
name = attr.is_a?(Arel::Attributes::Attribute) ? attr.name : attr.to_s
|
170
|
+
|
171
|
+
quoted = record.class.connection.quote_column_name(name)
|
172
|
+
"#{quoted} = (#{value.to_sql})"
|
173
|
+
end.join(', ')
|
174
|
+
end
|
175
|
+
|
176
|
+
def attribute(name)
|
177
|
+
name && Attribute.new(table, name.to_sym)
|
178
|
+
end
|
179
|
+
|
180
|
+
def expression(expr)
|
181
|
+
SqlLiteral.new(expr.to_s)
|
182
|
+
end
|
183
|
+
|
184
|
+
def id
|
185
|
+
attribute(record.ordered_tree.columns.id)
|
186
|
+
end
|
187
|
+
|
188
|
+
def parent_id
|
189
|
+
attribute(record.ordered_tree.columns.parent)
|
190
|
+
end
|
191
|
+
|
192
|
+
def position
|
193
|
+
attribute(record.ordered_tree.columns.position)
|
194
|
+
end
|
195
|
+
|
196
|
+
def depth
|
197
|
+
attribute(record.ordered_tree.columns.depth)
|
198
|
+
end
|
199
|
+
|
200
|
+
def table
|
201
|
+
record.class.arel_table
|
202
|
+
end
|
203
|
+
|
204
|
+
def method_missing(id, *args)
|
205
|
+
if args.length > 0
|
206
|
+
# function
|
207
|
+
NamedFunction.new(id.to_s.upcase, args)
|
208
|
+
else
|
209
|
+
super
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end # module DSL
|
213
|
+
end # module Transaction
|
214
|
+
end # module ActsAsOrderedTree
|