acts_as_recursive_tree 2.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/Appraisals
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
appraise 'ar-52' do
|
4
|
+
gem 'activerecord', '~> 5.2.0'
|
5
|
+
gem 'activesupport', '~> 5.2.0'
|
6
|
+
end
|
7
|
+
|
8
|
+
appraise 'ar-60' do
|
9
|
+
gem 'activerecord', '~> 6.0.0'
|
10
|
+
gem 'activesupport', '~> 6.0.0'
|
11
|
+
end
|
12
|
+
|
13
|
+
appraise 'ar-61' do
|
14
|
+
gem 'activerecord', '~> 6.1.0'
|
15
|
+
gem 'activesupport', '~> 6.1.0'
|
16
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
### Version 3.0.0
|
2
|
+
- BREAKING: Dropped support for Rails < 5.2
|
3
|
+
- BREAKING: Increased minimum Ruby version to 2.5
|
4
|
+
- ADD: initial support for Rails < 7
|
5
|
+
- CHANGE: Using zeitwerk for auto loading
|
6
|
+
|
7
|
+
### Version 2.2.1
|
8
|
+
- Rails 6.1 support
|
9
|
+
|
10
|
+
### Version 2.2.0
|
11
|
+
- Rails 6.0 support
|
12
|
+
|
13
|
+
### Version 2.1.1
|
14
|
+
- Enabled subselect query when using depth
|
15
|
+
- new QueryOption query_strategy for forcing a specific strategy (:join, :subselect)
|
16
|
+
|
1
17
|
### Version 2.1.0
|
2
18
|
- BUGFIX association self_and_siblings not working
|
3
19
|
- BUGFIX primary_key of model is retrieved on first usage and not on setup
|
@@ -25,4 +41,4 @@
|
|
25
41
|
- BUGFIX: ordering result when querying ancestors
|
26
42
|
|
27
43
|
### Version 1.0.0
|
28
|
-
-
|
44
|
+
- initial release using AREL
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# ActsAsRecursiveTree
|
2
2
|
|
3
|
+
[![CI Status](https://github.com/1and1/acts_as_recursive_tree/workflows/CI/badge.svg?branch=main)](https://github.com/1and1/acts_as_recursive_tree/actions?query=workflow%3ACI+branch%3Amaster)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/acts_as_recursive_tree.svg)](https://badge.fury.io/rb/acts_as_recursive_tree)
|
5
|
+
|
3
6
|
Use the power of recursive SQL statements in your Rails application.
|
4
7
|
|
5
8
|
When you have tree based data in your application, you always to struggle with retrieving data. There are solutions, but the always come at a price:
|
data/Rakefile
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/gem_tasks'
|
2
|
-
|
3
|
-
require 'rspec/core/rake_task'
|
4
|
-
RSpec::Core::RakeTask.new(:spec)
|
4
|
+
require 'rspec/core/rake_task'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
task default: [:spec]
|
9
9
|
|
10
10
|
desc 'Deletes temporary files'
|
11
11
|
task :clean_tmp_files do
|
12
|
-
%w
|
13
|
-
File.delete(file) if File.
|
12
|
+
%w[db.log test.sqlite3].each do |file|
|
13
|
+
File.delete(file) if File.exist?(file)
|
14
14
|
end
|
15
15
|
end
|
@@ -1,30 +1,39 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'acts_as_recursive_tree/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
This is a ruby gem that provides drop in replacement for acts_as_tree but makes use of SQL recursive statement. Be sure to have a DBMS that supports recursive queries when using this gem (e.g. PostgreSQL or SQLite).
|
14
|
-
spec.homepage
|
15
|
-
spec.license
|
16
|
-
|
17
|
-
|
8
|
+
spec.name = 'acts_as_recursive_tree'
|
9
|
+
spec.version = ActsAsRecursiveTree::VERSION
|
10
|
+
spec.authors = ['Wolfgang Wedelich-John', 'Willem Mulder']
|
11
|
+
spec.email = %w[wolfgang.wedelich@ionos.com 14mRh4X0r@gmail.com]
|
12
|
+
spec.summary = 'Drop in replacement for acts_as_tree but using recursive queries'
|
13
|
+
spec.description = '
|
14
|
+
This is a ruby gem that provides drop in replacement for acts_as_tree but makes use of SQL recursive statement. Be sure to have a DBMS that supports recursive queries when using this gem (e.g. PostgreSQL or SQLite). '
|
15
|
+
spec.homepage = 'https://github.com/1and1/acts_as_recursive_tree'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
spec.metadata = {
|
18
|
+
'bug_tracker_uri' => 'https://github.com/1and1/acts_as_recursive_tree/issues',
|
19
|
+
'changelog_uri' => 'https://github.com/1and1/acts_as_recursive_tree/CHANGELOG.md'
|
20
|
+
}
|
21
|
+
spec.required_ruby_version = '>= 2.5.0'
|
18
22
|
spec.files = `git ls-files -z`.split("\x0")
|
19
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
23
|
spec.test_files = spec.files.grep(%r{^spec/})
|
21
24
|
spec.require_paths = ['lib']
|
22
25
|
|
23
|
-
spec.add_runtime_dependency 'activerecord', '>= 5.
|
26
|
+
spec.add_runtime_dependency 'activerecord', '>= 5.2.0', '< 7.0'
|
27
|
+
spec.add_runtime_dependency 'activesupport', '>= 5.2.0', '< 7.0'
|
28
|
+
spec.add_runtime_dependency 'zeitwerk', '>= 2.4'
|
24
29
|
|
25
|
-
spec.add_development_dependency '
|
30
|
+
spec.add_development_dependency 'appraisal', '~> 2.4'
|
26
31
|
spec.add_development_dependency 'database_cleaner', '~> 1.5'
|
27
|
-
spec.add_development_dependency 'rake'
|
28
|
-
spec.add_development_dependency 'rspec-rails', '
|
32
|
+
spec.add_development_dependency 'rake'
|
33
|
+
spec.add_development_dependency 'rspec-rails', '>= 3.5'
|
34
|
+
spec.add_development_dependency 'rubocop', '>= 1.8.0'
|
35
|
+
spec.add_development_dependency 'rubocop-rails', '>= 2.9.0'
|
36
|
+
spec.add_development_dependency 'rubocop-rspec', '>= 2.1.0'
|
37
|
+
|
29
38
|
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
30
39
|
end
|
@@ -1,15 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require_relative 'acts_as_recursive_tree/railtie' if defined?(Rails)
|
4
|
+
require 'zeitwerk'
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.setup
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
autoload :Model
|
10
|
-
autoload :Associations
|
11
|
-
autoload :Scopes
|
12
|
-
autoload :Version
|
13
|
-
autoload :Options
|
14
|
-
autoload :Builders
|
9
|
+
module ActsAsRecursiveTree
|
10
|
+
# nothing special here
|
15
11
|
end
|
@@ -1,17 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module ActsMacro
|
3
|
-
|
4
5
|
##
|
5
6
|
# Configuration options are:
|
6
7
|
#
|
7
8
|
# * <tt>foreign_key</tt> - specifies the column name to use for tracking
|
8
9
|
# of the tree (default: +parent_id+)
|
9
10
|
def recursive_tree(parent_key: :parent_id, parent_type_column: nil)
|
10
|
-
|
11
11
|
class_attribute :_recursive_tree_config
|
12
12
|
self._recursive_tree_config = Config.new(
|
13
|
-
model_class:
|
14
|
-
parent_key:
|
13
|
+
model_class: self,
|
14
|
+
parent_key: parent_key.to_sym,
|
15
15
|
parent_type_column: parent_type_column.try(:to_sym)
|
16
16
|
)
|
17
17
|
|
@@ -20,6 +20,6 @@ module ActsAsRecursiveTree
|
|
20
20
|
include ActsAsRecursiveTree::Scopes
|
21
21
|
end
|
22
22
|
|
23
|
-
|
23
|
+
alias acts_as_tree recursive_tree
|
24
24
|
end
|
25
|
-
end
|
25
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
module ActsAsRecursiveTree
|
@@ -6,20 +8,20 @@ module ActsAsRecursiveTree
|
|
6
8
|
|
7
9
|
included do
|
8
10
|
belongs_to :parent,
|
9
|
-
class_name:
|
10
|
-
foreign_key:
|
11
|
-
inverse_of:
|
12
|
-
optional:
|
11
|
+
class_name: base_class.to_s,
|
12
|
+
foreign_key: _recursive_tree_config.parent_key,
|
13
|
+
inverse_of: :children,
|
14
|
+
optional: true
|
13
15
|
|
14
16
|
has_many :children,
|
15
|
-
class_name:
|
16
|
-
foreign_key:
|
17
|
-
inverse_of:
|
17
|
+
class_name: base_class.to_s,
|
18
|
+
foreign_key: _recursive_tree_config.parent_key,
|
19
|
+
inverse_of: :parent
|
18
20
|
|
19
21
|
has_many :self_and_siblings,
|
20
22
|
through: :parent,
|
21
23
|
source: :children,
|
22
|
-
class_name:
|
24
|
+
class_name: base_class.to_s
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
@@ -1,17 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
5
|
class Ancestors < RelationBuilder
|
4
|
-
|
5
|
-
def build_join_condition
|
6
|
-
travers_loc_table[parent_key].eq(base_table[primary_key])
|
7
|
-
end
|
6
|
+
self.traversal_strategy = ActsAsRecursiveTree::Builders::Strategies::Ancestor
|
8
7
|
|
9
8
|
def get_query_options(_)
|
10
9
|
opts = super
|
11
10
|
opts.ensure_ordering!
|
12
11
|
opts
|
13
12
|
end
|
14
|
-
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
5
|
class Descendants < RelationBuilder
|
4
|
-
|
5
|
-
base_table[parent_key].eq(travers_loc_table[primary_key])
|
6
|
-
end
|
6
|
+
self.traversal_strategy = ActsAsRecursiveTree::Builders::Strategies::Descendant
|
7
7
|
end
|
8
8
|
end
|
9
9
|
end
|
@@ -1,26 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
5
|
class Leaves < Descendants
|
4
|
-
|
5
6
|
def create_select_manger(column = nil)
|
6
7
|
select_manager = super
|
7
8
|
|
8
9
|
select_manager.where(
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
travers_loc_table[primary_key].not_in(
|
11
|
+
travers_loc_table.where(
|
12
|
+
travers_loc_table[parent_key].not_eq(nil)
|
13
|
+
).project(travers_loc_table[parent_key])
|
14
|
+
)
|
14
15
|
)
|
15
16
|
select_manager
|
16
|
-
|
17
17
|
end
|
18
18
|
|
19
19
|
def get_query_options(_)
|
20
20
|
# do not allow any custom options
|
21
21
|
ActsAsRecursiveTree::Options::QueryOptions.new
|
22
22
|
end
|
23
|
-
|
24
23
|
end
|
25
24
|
end
|
26
25
|
end
|
@@ -1,15 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActsAsRecursiveTree
|
2
4
|
module Builders
|
3
5
|
#
|
4
6
|
# Constructs the Arel necessary for recursion.
|
5
7
|
#
|
6
8
|
class RelationBuilder
|
7
|
-
|
8
9
|
def self.build(klass, ids, exclude_ids: false, &block)
|
9
10
|
new(klass, ids, exclude_ids: exclude_ids, &block).build
|
10
11
|
end
|
11
12
|
|
13
|
+
class_attribute :traversal_strategy, instance_writer: false
|
14
|
+
|
12
15
|
attr_reader :klass, :ids, :recursive_temp_table, :travers_loc_table, :without_ids
|
16
|
+
|
13
17
|
mattr_reader(:random) { Random.new }
|
14
18
|
|
15
19
|
# Delegators for easier accessing config and query options
|
@@ -39,7 +43,7 @@ module ActsAsRecursiveTree
|
|
39
43
|
def get_query_options(proc)
|
40
44
|
opts = ActsAsRecursiveTree::Options::QueryOptions.new
|
41
45
|
|
42
|
-
proc
|
46
|
+
proc&.call(opts)
|
43
47
|
|
44
48
|
opts
|
45
49
|
end
|
@@ -49,36 +53,33 @@ module ActsAsRecursiveTree
|
|
49
53
|
end
|
50
54
|
|
51
55
|
def build
|
52
|
-
relation =
|
56
|
+
relation = Strategies.for_query_options(@query_opts).build(self)
|
53
57
|
|
54
|
-
|
55
|
-
relation
|
58
|
+
apply_except_id(relation)
|
56
59
|
end
|
57
60
|
|
58
61
|
def apply_except_id(relation)
|
59
62
|
return relation unless without_ids
|
63
|
+
|
60
64
|
relation.where(ids.apply_negated_to(base_table[primary_key]))
|
61
65
|
end
|
62
66
|
|
63
|
-
def apply_depth(
|
64
|
-
return
|
65
|
-
|
66
|
-
relation.where(depth.apply_to(recursive_temp_table[depth_column]))
|
67
|
-
end
|
67
|
+
def apply_depth(select_manager)
|
68
|
+
return select_manager unless depth_present?
|
68
69
|
|
69
|
-
|
70
|
-
return relation unless ensure_ordering
|
71
|
-
relation.order(recursive_temp_table[depth_column].asc)
|
70
|
+
select_manager.where(depth.apply_to(travers_loc_table[depth_column]))
|
72
71
|
end
|
73
72
|
|
74
73
|
def create_select_manger(column = nil)
|
75
74
|
projections = if column
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
75
|
+
travers_loc_table[column]
|
76
|
+
else
|
77
|
+
Arel.star
|
78
|
+
end
|
79
|
+
|
80
|
+
select_mgr = travers_loc_table.project(projections).with(:recursive, build_cte_table)
|
80
81
|
|
81
|
-
|
82
|
+
apply_depth(select_mgr)
|
82
83
|
end
|
83
84
|
|
84
85
|
def build_cte_table
|
@@ -101,7 +102,9 @@ module ActsAsRecursiveTree
|
|
101
102
|
end
|
102
103
|
|
103
104
|
def build_union_select
|
104
|
-
join_condition = apply_parent_type_column(
|
105
|
+
join_condition = apply_parent_type_column(
|
106
|
+
traversal_strategy.build(self)
|
107
|
+
)
|
105
108
|
|
106
109
|
select_manager = base_table.join(travers_loc_table).on(join_condition)
|
107
110
|
|
@@ -113,7 +116,8 @@ module ActsAsRecursiveTree
|
|
113
116
|
end
|
114
117
|
|
115
118
|
def apply_parent_type_column(arel_condition)
|
116
|
-
return arel_condition
|
119
|
+
return arel_condition if parent_type_column.blank?
|
120
|
+
|
117
121
|
arel_condition.and(base_table[parent_type_column].eq(klass.base_class))
|
118
122
|
end
|
119
123
|
|
@@ -130,16 +134,9 @@ module ActsAsRecursiveTree
|
|
130
134
|
def apply_query_opts_condition(relation)
|
131
135
|
# check with nil? and not #present?/#blank? which will execute the query
|
132
136
|
return relation if condition.nil?
|
133
|
-
relation.merge(condition)
|
134
|
-
end
|
135
137
|
|
136
|
-
|
137
|
-
# U
|
138
|
-
#
|
139
|
-
def build_join_condition
|
140
|
-
raise 'not implemented'
|
138
|
+
relation.merge(condition)
|
141
139
|
end
|
142
|
-
|
143
140
|
end
|
144
141
|
end
|
145
142
|
end
|