acts_as_recursive_tree 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +321 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +175 -0
- data/Rakefile +15 -0
- data/acts_as_recursive_tree.gemspec +30 -0
- data/lib/acts_as_recursive_tree/acts_macro.rb +28 -0
- data/lib/acts_as_recursive_tree/associations.rb +25 -0
- data/lib/acts_as_recursive_tree/builders/ancestors.rb +17 -0
- data/lib/acts_as_recursive_tree/builders/descendants.rb +9 -0
- data/lib/acts_as_recursive_tree/builders/leaves.rb +26 -0
- data/lib/acts_as_recursive_tree/builders/relation_builder.rb +121 -0
- data/lib/acts_as_recursive_tree/builders.rb +13 -0
- data/lib/acts_as_recursive_tree/model.rb +125 -0
- data/lib/acts_as_recursive_tree/options/depth_condition.rb +48 -0
- data/lib/acts_as_recursive_tree/options/query_options.rb +21 -0
- data/lib/acts_as_recursive_tree/options/values.rb +77 -0
- data/lib/acts_as_recursive_tree/options.rb +9 -0
- data/lib/acts_as_recursive_tree/railtie.rb +9 -0
- data/lib/acts_as_recursive_tree/scopes.rb +16 -0
- data/lib/acts_as_recursive_tree/version.rb +3 -0
- data/lib/acts_as_recursive_tree.rb +14 -0
- data/spec/builders_spec.rb +136 -0
- data/spec/db/database.rb +22 -0
- data/spec/db/database.yml +12 -0
- data/spec/db/models.rb +37 -0
- data/spec/db/schema.rb +34 -0
- data/spec/model/location_spec.rb +55 -0
- data/spec/model/node_spec.rb +129 -0
- data/spec/model/relation_spec.rb +63 -0
- data/spec/spec_helper.rb +119 -0
- data/spec/values_spec.rb +86 -0
- metadata +182 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Builders
|
3
|
+
class Ancestors < RelationBuilder
|
4
|
+
|
5
|
+
def build_join_condition
|
6
|
+
travers_loc_table[config.parent_key].eq(base_table[config.primary_key])
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_query_options(_)
|
10
|
+
opts = super
|
11
|
+
opts.ensure_ordering!
|
12
|
+
opts
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Builders
|
3
|
+
class Leaves < Descendants
|
4
|
+
|
5
|
+
def create_select_manger
|
6
|
+
select_manager = super
|
7
|
+
|
8
|
+
select_manager.where(
|
9
|
+
travers_loc_table[config.primary_key].not_in(
|
10
|
+
travers_loc_table.where(
|
11
|
+
travers_loc_table[config.parent_key].not_eq(nil)
|
12
|
+
).project(travers_loc_table[config.parent_key])
|
13
|
+
)
|
14
|
+
)
|
15
|
+
select_manager
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_query_options(_)
|
20
|
+
# do not allow any custom options
|
21
|
+
ActsAsRecursiveTree::Options::QueryOptions.new
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Builders
|
3
|
+
class RelationBuilder
|
4
|
+
|
5
|
+
def self.build(klass, ids, exclude_ids: false, &block)
|
6
|
+
new(klass, ids, exclude_ids: exclude_ids, &block).build
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :klass, :ids, :recursive_temp_table, :travers_loc_table
|
10
|
+
attr_reader :query_opts, :without_ids
|
11
|
+
mattr_reader(:random) { Random.new }
|
12
|
+
|
13
|
+
def initialize(klass, ids, exclude_ids: false, &block)
|
14
|
+
@klass = klass
|
15
|
+
@ids = ActsAsRecursiveTree::Options::Values.create(ids, config)
|
16
|
+
@without_ids = exclude_ids
|
17
|
+
|
18
|
+
@query_opts = get_query_options(block)
|
19
|
+
|
20
|
+
rand_int = random.rand(1_000_000)
|
21
|
+
@recursive_temp_table = Arel::Table.new("recursive_#{klass.table_name}_#{rand_int}_temp")
|
22
|
+
@travers_loc_table = Arel::Table.new("traverse_#{rand_int}_loc")
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_query_options(proc)
|
26
|
+
opts = ActsAsRecursiveTree::Options::QueryOptions.new
|
27
|
+
|
28
|
+
proc.call(opts) if proc
|
29
|
+
|
30
|
+
opts
|
31
|
+
end
|
32
|
+
|
33
|
+
def base_table
|
34
|
+
klass.arel_table
|
35
|
+
end
|
36
|
+
|
37
|
+
def config
|
38
|
+
klass._recursive_tree_config
|
39
|
+
end
|
40
|
+
|
41
|
+
def build
|
42
|
+
final_select_mgr = base_table.join(
|
43
|
+
create_select_manger.as(recursive_temp_table.name)
|
44
|
+
).on(
|
45
|
+
base_table[config.primary_key].eq(recursive_temp_table[config.primary_key])
|
46
|
+
)
|
47
|
+
|
48
|
+
relation = klass.joins(final_select_mgr.join_sources)
|
49
|
+
|
50
|
+
relation = apply_except_id(relation)
|
51
|
+
relation = apply_depth(relation)
|
52
|
+
relation = apply_order(relation)
|
53
|
+
|
54
|
+
relation
|
55
|
+
end
|
56
|
+
|
57
|
+
def apply_except_id(relation)
|
58
|
+
return relation unless without_ids
|
59
|
+
relation.where(ids.apply_negated_to(base_table[config.primary_key]))
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_depth(relation)
|
63
|
+
return relation unless query_opts.depth_present?
|
64
|
+
|
65
|
+
relation.where(query_opts.depth.apply_to(recursive_temp_table[config.depth_column]))
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_order(relation)
|
69
|
+
return relation unless query_opts.ensure_ordering
|
70
|
+
relation.order(recursive_temp_table[config.depth_column].asc)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_select_manger
|
74
|
+
travers_loc_table.project(Arel.star).with(:recursive, build_cte_table)
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_cte_table
|
78
|
+
Arel::Nodes::As.new(
|
79
|
+
travers_loc_table,
|
80
|
+
build_base_select.union(build_union_select)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_base_select
|
85
|
+
id_node = base_table[config.primary_key]
|
86
|
+
|
87
|
+
base_table.where(
|
88
|
+
ids.apply_to(id_node)
|
89
|
+
).project(
|
90
|
+
id_node,
|
91
|
+
base_table[config.parent_key],
|
92
|
+
Arel.sql('0').as(config.depth_column.to_s)
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_union_select
|
97
|
+
select_manager = base_table.join(travers_loc_table).on(
|
98
|
+
build_join_condition
|
99
|
+
)
|
100
|
+
|
101
|
+
# need to use ActiveRecord here for merging relation
|
102
|
+
relation = klass.select(
|
103
|
+
base_table[config.primary_key],
|
104
|
+
base_table[config.parent_key],
|
105
|
+
Arel.sql(
|
106
|
+
(travers_loc_table[config.depth_column] + 1).to_sql
|
107
|
+
).as(config.depth_column.to_s)
|
108
|
+
).unscope(where: :type).joins(select_manager.join_sources)
|
109
|
+
|
110
|
+
relation = relation.merge(query_opts.condition) unless query_opts.condition.nil?
|
111
|
+
relation = relation.where(config.parent_type_column => klass.to_s) if config.parent_type_column
|
112
|
+
relation.arel
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_join_condition
|
116
|
+
raise 'not implemented'
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Builders
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
autoload :Values
|
6
|
+
autoload :DepthCondition
|
7
|
+
autoload :QueryOptions
|
8
|
+
autoload :RelationBuilder
|
9
|
+
autoload :Descendants
|
10
|
+
autoload :Ancestors
|
11
|
+
autoload :Leaves
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
##
|
6
|
+
# Returns list of ancestors, starting from parent until root.
|
7
|
+
#
|
8
|
+
# subchild1.ancestors # => [child1, root]
|
9
|
+
#
|
10
|
+
def ancestors(&block)
|
11
|
+
base_class.ancestors_of(self, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns ancestors and current node itself.
|
15
|
+
#
|
16
|
+
# subchild1.self_and_ancestors # => [subchild1, child1, root]
|
17
|
+
#
|
18
|
+
def self_and_ancestors(&block)
|
19
|
+
base_class.self_and_ancestors_of(self, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Returns list of descendants, starting from current node, not including current node.
|
24
|
+
#
|
25
|
+
# root.descendants # => [child1, child2, subchild1, subchild2, subchild3, subchild4]
|
26
|
+
#
|
27
|
+
def descendants(&block)
|
28
|
+
base_class.descendants_of(self, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns list of descendants, starting from current node, including current node.
|
33
|
+
#
|
34
|
+
# root.self_and_descendants # => [root, child1, child2, subchild1, subchild2, subchild3, subchild4]
|
35
|
+
#
|
36
|
+
def self_and_descendants(&block)
|
37
|
+
base_class.self_and_descendants_of(self, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Returns the root node of the tree.
|
42
|
+
def root
|
43
|
+
self_and_ancestors.where(self._recursive_tree_config.parent_key => nil).first
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Returns all siblings of the current node.
|
48
|
+
#
|
49
|
+
# subchild1.siblings # => [subchild2]
|
50
|
+
def siblings
|
51
|
+
self_and_siblings.where.not(id: self.id)
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns children (without subchildren) and current node itself.
|
56
|
+
#
|
57
|
+
# root.self_and_children # => [root, child1]
|
58
|
+
def self_and_children
|
59
|
+
table = self.class.arel_table
|
60
|
+
id = self.attributes[self._recursive_tree_config.primary_key.to_s]
|
61
|
+
|
62
|
+
base_class.where(
|
63
|
+
table[self._recursive_tree_config.primary_key].eq(id).or(
|
64
|
+
table[self._recursive_tree_config.parent_key].eq(id)
|
65
|
+
)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Returns all Leaves
|
71
|
+
#
|
72
|
+
def leaves
|
73
|
+
base_class.leaves_of(self)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Returns true if node has no parent, false otherwise
|
78
|
+
#
|
79
|
+
# subchild1.root? # => false
|
80
|
+
# root.root? # => true
|
81
|
+
def root?
|
82
|
+
self.attributes[self._recursive_tree_config.parent_key.to_s].blank?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns true if node has no children, false otherwise
|
86
|
+
#
|
87
|
+
# subchild1.leaf? # => true
|
88
|
+
# child1.leaf? # => false
|
89
|
+
def leaf?
|
90
|
+
!children.any?
|
91
|
+
end
|
92
|
+
|
93
|
+
def base_class
|
94
|
+
self.class.base_class
|
95
|
+
end
|
96
|
+
|
97
|
+
private :base_class
|
98
|
+
|
99
|
+
module ClassMethods
|
100
|
+
def self_and_ancestors_of(ids, &block)
|
101
|
+
Builders::Ancestors.build(self, ids, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
def ancestors_of(ids, &block)
|
105
|
+
Builders::Ancestors.build(self, ids, exclude_ids: true, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def roots_of(ids)
|
109
|
+
self_and_ancestors_of(ids).roots
|
110
|
+
end
|
111
|
+
|
112
|
+
def self_and_descendants_of(ids, &block)
|
113
|
+
Builders::Descendants.build(self, ids, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def descendants_of(ids, &block)
|
117
|
+
Builders::Descendants.build(self, ids, exclude_ids: true, &block)
|
118
|
+
end
|
119
|
+
|
120
|
+
def leaves_of(ids, &block)
|
121
|
+
Builders::Leaves.build(self, ids, &block)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Options
|
3
|
+
class DepthCondition
|
4
|
+
|
5
|
+
def ==(other)
|
6
|
+
@value = Values.create(other)
|
7
|
+
@operation = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def !=(other)
|
11
|
+
@value = Values.create(other)
|
12
|
+
@operation = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def <(other)
|
16
|
+
@value = other
|
17
|
+
@operation = :lt
|
18
|
+
end
|
19
|
+
|
20
|
+
def <=(other)
|
21
|
+
@value = other
|
22
|
+
@operation = :lteq
|
23
|
+
end
|
24
|
+
|
25
|
+
def >(other)
|
26
|
+
@value = other
|
27
|
+
@operation = :gt
|
28
|
+
end
|
29
|
+
|
30
|
+
def >=(other)
|
31
|
+
@value = other
|
32
|
+
@operation = :gteq
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply_to(attribute)
|
36
|
+
if @value.is_a?(Values::Base)
|
37
|
+
if @operation
|
38
|
+
@value.apply_to(attribute)
|
39
|
+
else
|
40
|
+
@value.apply_negated_to(attribute)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
attribute.send(@operation, @value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Options
|
3
|
+
class QueryOptions
|
4
|
+
|
5
|
+
attr_accessor :condition
|
6
|
+
attr_reader :ensure_ordering
|
7
|
+
|
8
|
+
def depth
|
9
|
+
@depth ||= DepthCondition.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def ensure_ordering!
|
13
|
+
@ensure_ordering = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def depth_present?
|
17
|
+
@depth.present?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Options
|
3
|
+
module Values
|
4
|
+
class Base
|
5
|
+
attr_reader :value, :config
|
6
|
+
|
7
|
+
def initialize(value, config)
|
8
|
+
@value = value
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def prepared_value
|
13
|
+
value
|
14
|
+
end
|
15
|
+
|
16
|
+
def apply_to(attribute)
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply_negated_to(attribute)
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class SingleValue < Base
|
26
|
+
def apply_to(attribute)
|
27
|
+
attribute.eq(prepared_value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply_negated_to(attribute)
|
31
|
+
attribute.not_eq(prepared_value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class ActiveRecord < SingleValue
|
36
|
+
def prepared_value
|
37
|
+
value.id
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class MultiValue < Base
|
42
|
+
def apply_to(attribute)
|
43
|
+
attribute.in(prepared_value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply_negated_to(attribute)
|
47
|
+
attribute.not_in(prepared_value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Relation < MultiValue
|
52
|
+
def prepared_value
|
53
|
+
select_manager = value.arel
|
54
|
+
select_manager.projections.clear
|
55
|
+
select_manager.project(select_manager.froms.last[config.primary_key])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.create(value, config = nil)
|
60
|
+
klass = case value
|
61
|
+
when ::Numeric, ::String
|
62
|
+
SingleValue
|
63
|
+
when ::ActiveRecord::Relation
|
64
|
+
Relation
|
65
|
+
when Enumerable
|
66
|
+
MultiValue
|
67
|
+
when ::ActiveRecord::Base
|
68
|
+
ActiveRecord
|
69
|
+
else
|
70
|
+
raise "#{value.class} is not supported"
|
71
|
+
end
|
72
|
+
|
73
|
+
klass.new(value, config)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActsAsRecursiveTree
|
2
|
+
module Scopes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
scope :roots, -> {
|
7
|
+
rel = where(_recursive_tree_config.parent_key => nil)
|
8
|
+
rel = rel.or(
|
9
|
+
where.not(_recursive_tree_config.parent_type_column => self.to_s)
|
10
|
+
) if _recursive_tree_config.parent_type_column
|
11
|
+
|
12
|
+
rel
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require_relative 'acts_as_recursive_tree/railtie' if defined?(Rails)
|
3
|
+
|
4
|
+
module ActsAsRecursiveTree
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :ActsMacro
|
8
|
+
autoload :Model
|
9
|
+
autoload :Associations
|
10
|
+
autoload :Scopes
|
11
|
+
autoload :Version
|
12
|
+
autoload :Options
|
13
|
+
autoload :Builders
|
14
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_context 'setup with enforced ordering' do
|
4
|
+
let(:ordering) { false }
|
5
|
+
include_context 'base_setup' do
|
6
|
+
let(:proc) { -> (config) { config.ensure_ordering! } }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
shared_context 'base_setup' do
|
11
|
+
let(:model_id) { 1 }
|
12
|
+
let(:model_class) { Node }
|
13
|
+
let(:exclude_ids) { false }
|
14
|
+
let(:proc) { nil }
|
15
|
+
let(:builder) do
|
16
|
+
described_class.new(model_class, model_id, exclude_ids: exclude_ids, &proc)
|
17
|
+
end
|
18
|
+
subject(:query) { builder.build.to_sql }
|
19
|
+
end
|
20
|
+
|
21
|
+
shared_examples 'basic recursive examples' do
|
22
|
+
it { is_expected.to start_with "SELECT \"#{model_class.table_name}\".* FROM \"#{model_class.table_name}\"" }
|
23
|
+
it { is_expected.to match /WHERE "#{model_class.table_name}"."#{model_class.primary_key}" = #{model_id}/ }
|
24
|
+
it { is_expected.to match /WITH RECURSIVE "#{builder.travers_loc_table.name}" AS/ }
|
25
|
+
it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", 0 AS recursive_depth FROM "#{model_class.table_name}"/ }
|
26
|
+
it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", \("#{builder.travers_loc_table.name}"."recursive_depth" \+ 1\) AS recursive_depth FROM "#{model_class.table_name}"/ }
|
27
|
+
it { is_expected.to match /#{Regexp.escape(builder.travers_loc_table.project(Arel.star).to_sql)}/ }
|
28
|
+
it { is_expected.to match /"#{model_class.table_name}"."#{model_class.primary_key}" = "#{builder.recursive_temp_table.name}"."#{model_class.primary_key}"/ }
|
29
|
+
end
|
30
|
+
|
31
|
+
shared_examples 'build recursive query' do
|
32
|
+
context 'simple id' do
|
33
|
+
context 'with simple class' do
|
34
|
+
include_context 'base_setup' do
|
35
|
+
let(:model_class) { Node }
|
36
|
+
it_behaves_like 'basic recursive examples'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with class with different parent key' do
|
41
|
+
include_context 'base_setup' do
|
42
|
+
let(:model_class) { NodeWithOtherParentKey }
|
43
|
+
it_behaves_like 'basic recursive examples'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with Subclass' do
|
48
|
+
include_context 'base_setup' do
|
49
|
+
let(:model_class) { Floor }
|
50
|
+
it_behaves_like 'basic recursive examples'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'with polymorphic parent relation' do
|
55
|
+
include_context 'base_setup' do
|
56
|
+
let(:model_class) { NodeWithPolymorphicParent }
|
57
|
+
it_behaves_like 'basic recursive examples'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
shared_examples 'ancestor query' do
|
64
|
+
include_context 'base_setup'
|
65
|
+
|
66
|
+
it { is_expected.to match /"#{builder.travers_loc_table.name}"."#{model_class._recursive_tree_config.parent_key}" = "#{model_class.table_name}"."#{model_class.primary_key}"/ }
|
67
|
+
end
|
68
|
+
|
69
|
+
shared_examples 'descendant query' do
|
70
|
+
include_context 'base_setup'
|
71
|
+
|
72
|
+
it { is_expected.to match /"#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}" = "#{builder.travers_loc_table.name}"."#{model_class.primary_key}"/ }
|
73
|
+
end
|
74
|
+
|
75
|
+
shared_context 'context with ordering' do
|
76
|
+
include_context 'base_setup' do
|
77
|
+
it_behaves_like 'with ordering'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
shared_context 'context without ordering' do
|
82
|
+
include_context 'base_setup' do
|
83
|
+
it_behaves_like 'without ordering'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
shared_examples 'with ordering' do
|
88
|
+
it { is_expected.to match /ORDER BY #{Regexp.escape(builder.recursive_temp_table[model_class._recursive_tree_config.depth_column].asc.to_sql)}/ }
|
89
|
+
end
|
90
|
+
|
91
|
+
shared_examples 'without ordering' do
|
92
|
+
it { is_expected.to_not match /ORDER BY/ }
|
93
|
+
end
|
94
|
+
|
95
|
+
describe ActsAsRecursiveTree::Builders::Descendants do
|
96
|
+
context 'basic' do
|
97
|
+
it_behaves_like 'build recursive query'
|
98
|
+
it_behaves_like 'descendant query'
|
99
|
+
include_context 'context without ordering'
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'with options' do
|
103
|
+
include_context 'setup with enforced ordering' do
|
104
|
+
let(:ordering) { true }
|
105
|
+
it_behaves_like 'with ordering'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe ActsAsRecursiveTree::Builders::Ancestors do
|
111
|
+
context 'basic' do
|
112
|
+
it_behaves_like 'build recursive query'
|
113
|
+
it_behaves_like 'ancestor query'
|
114
|
+
include_context 'context with ordering'
|
115
|
+
end
|
116
|
+
context 'with options' do
|
117
|
+
include_context 'setup with enforced ordering' do
|
118
|
+
it_behaves_like 'with ordering'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe ActsAsRecursiveTree::Builders::Leaves do
|
124
|
+
context 'basic' do
|
125
|
+
it_behaves_like 'build recursive query'
|
126
|
+
it_behaves_like 'descendant query'
|
127
|
+
include_context 'context without ordering'
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'with options' do
|
131
|
+
include_context 'setup with enforced ordering' do
|
132
|
+
let(:ordering) { true }
|
133
|
+
it_behaves_like 'without ordering'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/spec/db/database.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
database_folder = "#{File.dirname(__FILE__)}/../db"
|
2
|
+
database_adapter = 'sqlite'
|
3
|
+
|
4
|
+
# Logger setup
|
5
|
+
ActiveRecord::Base.logger = nil
|
6
|
+
|
7
|
+
ActiveRecord::Migration.verbose = false
|
8
|
+
|
9
|
+
ActiveRecord::Base.configurations = YAML::load(File.read("#{database_folder}/database.yml"))
|
10
|
+
|
11
|
+
config = ActiveRecord::Base.configurations[database_adapter]
|
12
|
+
|
13
|
+
# remove database if present
|
14
|
+
FileUtils.rm config['database'], force: true
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection(database_adapter.to_sym)
|
17
|
+
ActiveRecord::Base.establish_connection(config)
|
18
|
+
|
19
|
+
# require schemata and models
|
20
|
+
require_relative 'schema'
|
21
|
+
require_relative 'models'
|
22
|
+
|