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.
- 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
|