acts_as_recursive_tree 2.1.0 → 3.0.0
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/.github/workflows/ci.yml +37 -0
- data/.github/workflows/lint.yml +31 -0
- data/.github/workflows/rubygem.yml +37 -0
- data/.gitignore +3 -1
- data/.rubocop.yml +30 -1
- data/.rubocop_todo.yml +28 -281
- data/Appraisals +16 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile +2 -0
- data/README.md +3 -0
- data/Rakefile +8 -8
- data/acts_as_recursive_tree.gemspec +27 -18
- data/gemfiles/ar_52.gemfile +8 -0
- data/gemfiles/ar_60.gemfile +8 -0
- data/gemfiles/ar_61.gemfile +8 -0
- data/lib/acts_as_recursive_tree.rb +7 -11
- data/lib/acts_as_recursive_tree/acts_macro.rb +6 -6
- data/lib/acts_as_recursive_tree/associations.rb +10 -8
- data/lib/acts_as_recursive_tree/builders/ancestors.rb +3 -5
- data/lib/acts_as_recursive_tree/builders/descendants.rb +3 -3
- data/lib/acts_as_recursive_tree/builders/leaves.rb +7 -8
- data/lib/acts_as_recursive_tree/builders/relation_builder.rb +25 -28
- data/lib/acts_as_recursive_tree/builders/{strategy.rb → strategies.rb} +4 -7
- data/lib/acts_as_recursive_tree/builders/strategies/ancestor.rb +19 -0
- data/lib/acts_as_recursive_tree/builders/strategies/descendant.rb +19 -0
- data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/join.rb +10 -4
- data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/subselect.rb +3 -1
- data/lib/acts_as_recursive_tree/config.rb +2 -0
- data/lib/acts_as_recursive_tree/model.rb +9 -8
- data/lib/acts_as_recursive_tree/options/depth_condition.rb +3 -2
- data/lib/acts_as_recursive_tree/options/query_options.rb +10 -1
- data/lib/acts_as_recursive_tree/options/values.rb +28 -18
- data/lib/acts_as_recursive_tree/railtie.rb +2 -0
- data/lib/acts_as_recursive_tree/scopes.rb +8 -4
- data/lib/acts_as_recursive_tree/version.rb +3 -1
- data/spec/builders_spec.rb +21 -12
- data/spec/db/database.rb +11 -4
- data/spec/db/database.yml +2 -5
- data/spec/db/models.rb +12 -11
- data/spec/db/schema.rb +3 -4
- data/spec/model/location_spec.rb +7 -11
- data/spec/model/node_spec.rb +35 -49
- data/spec/model/relation_spec.rb +6 -11
- data/spec/spec_helper.rb +54 -55
- data/spec/values_spec.rb +23 -19
- metadata +111 -25
- data/lib/acts_as_recursive_tree/builders.rb +0 -14
- data/lib/acts_as_recursive_tree/options.rb +0 -9
@@ -1,14 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
5
|
#
|
4
6
|
# Strategy module for different strategies of how to build the resulting query.
|
5
7
|
#
|
6
|
-
module
|
7
|
-
extend ActiveSupport::Autoload
|
8
|
-
|
9
|
-
autoload :Join
|
10
|
-
autoload :Subselect
|
11
|
-
|
8
|
+
module Strategies
|
12
9
|
#
|
13
10
|
# Returns a Strategy appropriate for query_opts
|
14
11
|
#
|
@@ -16,7 +13,7 @@ module ActsAsRecursiveTree
|
|
16
13
|
#
|
17
14
|
# @return a strategy class best suited for the opts
|
18
15
|
def self.for_query_options(query_opts)
|
19
|
-
if query_opts.
|
16
|
+
if query_opts.ensure_ordering || query_opts.query_strategy == :join
|
20
17
|
Join
|
21
18
|
else
|
22
19
|
Subselect
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsRecursiveTree
|
4
|
+
module Builders
|
5
|
+
module Strategies
|
6
|
+
#
|
7
|
+
# Strategy for building ancestors relation
|
8
|
+
#
|
9
|
+
module Ancestor
|
10
|
+
#
|
11
|
+
# Builds the relation
|
12
|
+
#
|
13
|
+
def self.build(builder)
|
14
|
+
builder.travers_loc_table[builder.parent_key].eq(builder.base_table[builder.primary_key])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsRecursiveTree
|
4
|
+
module Builders
|
5
|
+
module Strategies
|
6
|
+
#
|
7
|
+
# Strategy for building descendants relation
|
8
|
+
#
|
9
|
+
module Descendant
|
10
|
+
#
|
11
|
+
# Builds the relation
|
12
|
+
#
|
13
|
+
def self.build(builder)
|
14
|
+
builder.base_table[builder.parent_key].eq(builder.travers_loc_table[builder.primary_key])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
|
-
module
|
5
|
+
module Strategies
|
4
6
|
#
|
5
7
|
# Build a relation using an INNER JOIN.
|
6
8
|
#
|
@@ -19,9 +21,13 @@ module ActsAsRecursiveTree
|
|
19
21
|
|
20
22
|
relation = builder.klass.joins(final_select_mgr.join_sources)
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
apply_order(builder, relation)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.apply_order(builder, relation)
|
28
|
+
return relation unless builder.ensure_ordering
|
29
|
+
|
30
|
+
relation.order(builder.recursive_temp_table[builder.depth_column].asc)
|
25
31
|
end
|
26
32
|
end
|
27
33
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Model
|
3
5
|
extend ActiveSupport::Concern
|
@@ -40,7 +42,7 @@ module ActsAsRecursiveTree
|
|
40
42
|
##
|
41
43
|
# Returns the root node of the tree.
|
42
44
|
def root
|
43
|
-
self_and_ancestors.where(
|
45
|
+
self_and_ancestors.where(_recursive_tree_config.parent_key => nil).first
|
44
46
|
end
|
45
47
|
|
46
48
|
##
|
@@ -48,7 +50,7 @@ module ActsAsRecursiveTree
|
|
48
50
|
#
|
49
51
|
# subchild1.siblings # => [subchild2]
|
50
52
|
def siblings
|
51
|
-
self_and_siblings.where.not(id:
|
53
|
+
self_and_siblings.where.not(id: id)
|
52
54
|
end
|
53
55
|
|
54
56
|
##
|
@@ -57,11 +59,11 @@ module ActsAsRecursiveTree
|
|
57
59
|
# root.self_and_children # => [root, child1]
|
58
60
|
def self_and_children
|
59
61
|
table = self.class.arel_table
|
60
|
-
id =
|
62
|
+
id = attributes[_recursive_tree_config.primary_key.to_s]
|
61
63
|
|
62
64
|
base_class.where(
|
63
|
-
table[
|
64
|
-
table[
|
65
|
+
table[_recursive_tree_config.primary_key].eq(id).or(
|
66
|
+
table[_recursive_tree_config.parent_key].eq(id)
|
65
67
|
)
|
66
68
|
)
|
67
69
|
end
|
@@ -73,13 +75,12 @@ module ActsAsRecursiveTree
|
|
73
75
|
base_class.leaves_of(self)
|
74
76
|
end
|
75
77
|
|
76
|
-
|
77
78
|
# Returns true if node has no parent, false otherwise
|
78
79
|
#
|
79
80
|
# subchild1.root? # => false
|
80
81
|
# root.root? # => true
|
81
82
|
def root?
|
82
|
-
|
83
|
+
attributes[_recursive_tree_config.parent_key.to_s].blank?
|
83
84
|
end
|
84
85
|
|
85
86
|
# Returns true if node has no children, false otherwise
|
@@ -87,7 +88,7 @@ module ActsAsRecursiveTree
|
|
87
88
|
# subchild1.leaf? # => true
|
88
89
|
# child1.leaf? # => false
|
89
90
|
def leaf?
|
90
|
-
|
91
|
+
children.none?
|
91
92
|
end
|
92
93
|
|
93
94
|
def base_class
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Options
|
3
5
|
class QueryOptions
|
6
|
+
STRATEGIES = %i[subselect join].freeze
|
4
7
|
|
5
8
|
attr_accessor :condition
|
6
|
-
attr_reader :ensure_ordering
|
9
|
+
attr_reader :ensure_ordering, :query_strategy
|
7
10
|
|
8
11
|
def depth
|
9
12
|
@depth ||= DepthCondition.new
|
@@ -16,6 +19,12 @@ module ActsAsRecursiveTree
|
|
16
19
|
def depth_present?
|
17
20
|
@depth.present?
|
18
21
|
end
|
22
|
+
|
23
|
+
def query_strategy=(strategy)
|
24
|
+
raise "invalid strategy #{strategy} - only #{STRATEGIES} are allowed" unless STRATEGIES.include?(strategy)
|
25
|
+
|
26
|
+
@query_strategy = strategy
|
27
|
+
end
|
19
28
|
end
|
20
29
|
end
|
21
30
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Options
|
3
5
|
module Values
|
@@ -13,13 +15,9 @@ module ActsAsRecursiveTree
|
|
13
15
|
value
|
14
16
|
end
|
15
17
|
|
16
|
-
def apply_to(attribute)
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
def apply_negated_to(attribute)
|
18
|
+
def apply_to(attribute); end
|
21
19
|
|
22
|
-
end
|
20
|
+
def apply_negated_to(attribute); end
|
23
21
|
end
|
24
22
|
|
25
23
|
class SingleValue < Base
|
@@ -38,6 +36,16 @@ module ActsAsRecursiveTree
|
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
39
|
+
class RangeValue < Base
|
40
|
+
def apply_to(attribute)
|
41
|
+
attribute.between(prepared_value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_negated_to(attribute)
|
45
|
+
attribute.not_between(prepared_value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
41
49
|
class MultiValue < Base
|
42
50
|
def apply_to(attribute)
|
43
51
|
attribute.in(prepared_value)
|
@@ -58,20 +66,22 @@ module ActsAsRecursiveTree
|
|
58
66
|
|
59
67
|
def self.create(value, config = nil)
|
60
68
|
klass = case value
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
when ::Numeric, ::String
|
70
|
+
SingleValue
|
71
|
+
when ::ActiveRecord::Relation
|
72
|
+
Relation
|
73
|
+
when Range
|
74
|
+
RangeValue
|
75
|
+
when Enumerable
|
76
|
+
MultiValue
|
77
|
+
when ::ActiveRecord::Base
|
78
|
+
ActiveRecord
|
79
|
+
else
|
80
|
+
raise "#{value.class} is not supported"
|
81
|
+
end
|
72
82
|
|
73
83
|
klass.new(value, config)
|
74
84
|
end
|
75
85
|
end
|
76
86
|
end
|
77
|
-
end
|
87
|
+
end
|
@@ -1,13 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Scopes
|
3
5
|
extend ActiveSupport::Concern
|
4
6
|
|
5
7
|
included do
|
6
|
-
scope :roots,
|
8
|
+
scope :roots, lambda {
|
7
9
|
rel = where(_recursive_tree_config.parent_key => nil)
|
8
|
-
|
9
|
-
|
10
|
-
|
10
|
+
if _recursive_tree_config.parent_type_column
|
11
|
+
rel = rel.or(
|
12
|
+
where.not(_recursive_tree_config.parent_type_column => to_s)
|
13
|
+
)
|
14
|
+
end
|
11
15
|
|
12
16
|
rel
|
13
17
|
}
|
data/spec/builders_spec.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
shared_context 'setup with enforced ordering' do
|
4
6
|
let(:ordering) { false }
|
5
7
|
include_context 'base_setup' do
|
6
|
-
let(:proc) { ->
|
8
|
+
let(:proc) { ->(config) { config.ensure_ordering! } }
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
10
12
|
shared_context 'base_setup' do
|
13
|
+
subject(:query) { builder.build.to_sql }
|
14
|
+
|
11
15
|
let(:model_id) { 1 }
|
12
16
|
let(:model_class) { Node }
|
13
17
|
let(:exclude_ids) { false }
|
@@ -15,17 +19,20 @@ shared_context 'base_setup' do
|
|
15
19
|
let(:builder) do
|
16
20
|
described_class.new(model_class, model_id, exclude_ids: exclude_ids, &proc)
|
17
21
|
end
|
18
|
-
subject(:query) { builder.build.to_sql }
|
19
22
|
end
|
20
23
|
|
21
24
|
shared_examples 'basic recursive examples' do
|
22
25
|
it { is_expected.to start_with "SELECT \"#{model_class.table_name}\".* FROM \"#{model_class.table_name}\"" }
|
23
|
-
|
24
|
-
it { is_expected.to match
|
25
|
-
|
26
|
-
it { is_expected.to match
|
27
|
-
|
28
|
-
it { is_expected.to match
|
26
|
+
|
27
|
+
it { is_expected.to match(/WHERE "#{model_class.table_name}"."#{model_class.primary_key}" = #{model_id}/) }
|
28
|
+
|
29
|
+
it { is_expected.to match(/WITH RECURSIVE "#{builder.travers_loc_table.name}" AS/) }
|
30
|
+
|
31
|
+
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}"/) }
|
32
|
+
|
33
|
+
it {
|
34
|
+
expect(subject).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}"/)
|
35
|
+
}
|
29
36
|
end
|
30
37
|
|
31
38
|
shared_examples 'build recursive query' do
|
@@ -63,13 +70,14 @@ end
|
|
63
70
|
shared_examples 'ancestor query' do
|
64
71
|
include_context 'base_setup'
|
65
72
|
|
66
|
-
it { is_expected.to match
|
73
|
+
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
74
|
end
|
68
75
|
|
69
76
|
shared_examples 'descendant query' do
|
70
77
|
include_context 'base_setup'
|
71
78
|
|
72
|
-
it { is_expected.to match
|
79
|
+
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}"/) }
|
80
|
+
it { is_expected.to match(/#{Regexp.escape(builder.travers_loc_table.project(builder.travers_loc_table[model_class.primary_key]).to_sql)}/) }
|
73
81
|
end
|
74
82
|
|
75
83
|
shared_context 'context with ordering' do
|
@@ -85,11 +93,11 @@ shared_context 'context without ordering' do
|
|
85
93
|
end
|
86
94
|
|
87
95
|
shared_examples 'with ordering' do
|
88
|
-
it { is_expected.to match
|
96
|
+
it { is_expected.to match(/ORDER BY #{Regexp.escape(builder.recursive_temp_table[model_class._recursive_tree_config.depth_column].asc.to_sql)}/) }
|
89
97
|
end
|
90
98
|
|
91
99
|
shared_examples 'without ordering' do
|
92
|
-
it { is_expected.
|
100
|
+
it { is_expected.not_to match(/ORDER BY/) }
|
93
101
|
end
|
94
102
|
|
95
103
|
describe ActsAsRecursiveTree::Builders::Descendants do
|
@@ -113,6 +121,7 @@ describe ActsAsRecursiveTree::Builders::Ancestors do
|
|
113
121
|
it_behaves_like 'ancestor query'
|
114
122
|
include_context 'context with ordering'
|
115
123
|
end
|
124
|
+
|
116
125
|
context 'with options' do
|
117
126
|
include_context 'setup with enforced ordering' do
|
118
127
|
it_behaves_like 'with ordering'
|
data/spec/db/database.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
database_folder = "#{File.dirname(__FILE__)}/../db"
|
2
4
|
database_adapter = 'sqlite'
|
3
5
|
|
@@ -6,12 +8,18 @@ ActiveRecord::Base.logger = nil
|
|
6
8
|
|
7
9
|
ActiveRecord::Migration.verbose = false
|
8
10
|
|
9
|
-
ActiveRecord::Base.configurations = YAML
|
11
|
+
ActiveRecord::Base.configurations = YAML.safe_load(File.read("#{database_folder}/database.yml"))
|
10
12
|
|
11
|
-
|
13
|
+
if ActiveRecord.version >= Gem::Version.new('6.1.0')
|
14
|
+
config = ActiveRecord::Base.configurations.configs_for env_name: database_adapter, name: 'primary'
|
15
|
+
database = config.database
|
16
|
+
else
|
17
|
+
config = ActiveRecord::Base.configurations[database_adapter]
|
18
|
+
database = config['database']
|
19
|
+
end
|
12
20
|
|
13
21
|
# remove database if present
|
14
|
-
FileUtils.rm
|
22
|
+
FileUtils.rm database, force: true
|
15
23
|
|
16
24
|
ActiveRecord::Base.establish_connection(database_adapter.to_sym)
|
17
25
|
ActiveRecord::Base.establish_connection(config)
|
@@ -19,4 +27,3 @@ ActiveRecord::Base.establish_connection(config)
|
|
19
27
|
# require schemata and models
|
20
28
|
require_relative 'schema'
|
21
29
|
require_relative 'models'
|
22
|
-
|