acts_as_ordered_tree 1.3.1 → 2.0.0.beta3

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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/lib/acts_as_ordered_tree.rb +22 -100
  3. data/lib/acts_as_ordered_tree/adapters.rb +17 -0
  4. data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
  5. data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
  6. data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
  7. data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
  8. data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
  9. data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
  10. data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
  11. data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
  12. data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
  13. data/lib/acts_as_ordered_tree/hooks.rb +38 -0
  14. data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
  15. data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
  16. data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
  17. data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
  18. data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
  19. data/lib/acts_as_ordered_tree/node.rb +78 -0
  20. data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
  21. data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
  22. data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
  23. data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
  24. data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
  25. data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
  26. data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
  27. data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
  28. data/lib/acts_as_ordered_tree/position.rb +143 -0
  29. data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
  30. data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
  31. data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
  32. data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
  33. data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
  34. data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
  35. data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
  36. data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
  37. data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
  38. data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
  39. data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
  40. data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
  41. data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
  42. data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
  43. data/lib/acts_as_ordered_tree/tree.rb +148 -0
  44. data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
  45. data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
  46. data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
  47. data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
  48. data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
  49. data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
  50. data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
  51. data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
  52. data/lib/acts_as_ordered_tree/validators.rb +1 -1
  53. data/lib/acts_as_ordered_tree/version.rb +1 -1
  54. data/spec/acts_as_ordered_tree_spec.rb +80 -909
  55. data/spec/adapters/postgresql_spec.rb +14 -0
  56. data/spec/adapters/recursive_spec.rb +12 -0
  57. data/spec/adapters/shared.rb +272 -0
  58. data/spec/callbacks_spec.rb +177 -0
  59. data/spec/counter_cache_spec.rb +31 -0
  60. data/spec/create_spec.rb +110 -0
  61. data/spec/destroy_spec.rb +57 -0
  62. data/spec/inheritance_spec.rb +176 -0
  63. data/spec/move_spec.rb +94 -0
  64. data/spec/node/movements/concurrent_movements_spec.rb +354 -0
  65. data/spec/node/movements/move_higher_spec.rb +46 -0
  66. data/spec/node/movements/move_lower_spec.rb +46 -0
  67. data/spec/node/movements/move_to_child_of_spec.rb +147 -0
  68. data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
  69. data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
  70. data/spec/node/movements/move_to_left_of_spec.rb +120 -0
  71. data/spec/node/movements/move_to_right_of_spec.rb +120 -0
  72. data/spec/node/movements/move_to_root_spec.rb +67 -0
  73. data/spec/node/predicates_spec.rb +211 -0
  74. data/spec/node/reloading_spec.rb +42 -0
  75. data/spec/node/siblings_spec.rb +193 -0
  76. data/spec/node/traversals_spec.rb +71 -0
  77. data/spec/persevering_transaction_spec.rb +98 -0
  78. data/spec/relation/arrangeable_spec.rb +88 -0
  79. data/spec/relation/iterable_spec.rb +104 -0
  80. data/spec/relation/preloaded_spec.rb +57 -0
  81. data/spec/reorder_spec.rb +83 -0
  82. data/spec/spec_helper.rb +30 -38
  83. data/spec/support/db/boot.rb +22 -0
  84. data/spec/{db → support/db}/config.travis.yml +2 -0
  85. data/spec/{db → support/db}/config.yml +1 -0
  86. data/spec/{db → support/db}/schema.rb +9 -0
  87. data/spec/support/factories.rb +2 -2
  88. data/spec/support/matchers.rb +67 -58
  89. data/spec/support/models.rb +6 -14
  90. data/spec/support/tree_factory.rb +315 -0
  91. data/spec/tree/children_association_spec.rb +72 -0
  92. data/spec/tree/columns_spec.rb +65 -0
  93. data/spec/tree/scopes_spec.rb +39 -0
  94. metadata +161 -43
  95. data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
  96. data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
  97. data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
  98. data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
  99. data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
  100. 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
- require "acts_as_ordered_tree/relation/base"
1
+ # coding: utf-8
2
2
 
3
3
  module ActsAsOrderedTree
4
4
  module Relation
5
- # Common relation, but with already loaded records
6
- class Preloaded < Base
7
- # Set loaded records to +records+
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
- relation = clone
10
- relation.instance_variable_set :@records, records
11
- relation.instance_variable_set :@loaded, true
12
- relation
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