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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +37 -0
  3. data/.github/workflows/lint.yml +31 -0
  4. data/.github/workflows/rubygem.yml +37 -0
  5. data/.gitignore +3 -1
  6. data/.rubocop.yml +30 -1
  7. data/.rubocop_todo.yml +28 -281
  8. data/Appraisals +16 -0
  9. data/CHANGELOG.md +17 -1
  10. data/Gemfile +2 -0
  11. data/README.md +3 -0
  12. data/Rakefile +8 -8
  13. data/acts_as_recursive_tree.gemspec +27 -18
  14. data/gemfiles/ar_52.gemfile +8 -0
  15. data/gemfiles/ar_60.gemfile +8 -0
  16. data/gemfiles/ar_61.gemfile +8 -0
  17. data/lib/acts_as_recursive_tree.rb +7 -11
  18. data/lib/acts_as_recursive_tree/acts_macro.rb +6 -6
  19. data/lib/acts_as_recursive_tree/associations.rb +10 -8
  20. data/lib/acts_as_recursive_tree/builders/ancestors.rb +3 -5
  21. data/lib/acts_as_recursive_tree/builders/descendants.rb +3 -3
  22. data/lib/acts_as_recursive_tree/builders/leaves.rb +7 -8
  23. data/lib/acts_as_recursive_tree/builders/relation_builder.rb +25 -28
  24. data/lib/acts_as_recursive_tree/builders/{strategy.rb → strategies.rb} +4 -7
  25. data/lib/acts_as_recursive_tree/builders/strategies/ancestor.rb +19 -0
  26. data/lib/acts_as_recursive_tree/builders/strategies/descendant.rb +19 -0
  27. data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/join.rb +10 -4
  28. data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/subselect.rb +3 -1
  29. data/lib/acts_as_recursive_tree/config.rb +2 -0
  30. data/lib/acts_as_recursive_tree/model.rb +9 -8
  31. data/lib/acts_as_recursive_tree/options/depth_condition.rb +3 -2
  32. data/lib/acts_as_recursive_tree/options/query_options.rb +10 -1
  33. data/lib/acts_as_recursive_tree/options/values.rb +28 -18
  34. data/lib/acts_as_recursive_tree/railtie.rb +2 -0
  35. data/lib/acts_as_recursive_tree/scopes.rb +8 -4
  36. data/lib/acts_as_recursive_tree/version.rb +3 -1
  37. data/spec/builders_spec.rb +21 -12
  38. data/spec/db/database.rb +11 -4
  39. data/spec/db/database.yml +2 -5
  40. data/spec/db/models.rb +12 -11
  41. data/spec/db/schema.rb +3 -4
  42. data/spec/model/location_spec.rb +7 -11
  43. data/spec/model/node_spec.rb +35 -49
  44. data/spec/model/relation_spec.rb +6 -11
  45. data/spec/spec_helper.rb +54 -55
  46. data/spec/values_spec.rb +23 -19
  47. metadata +111 -25
  48. data/lib/acts_as_recursive_tree/builders.rb +0 -14
  49. 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
- - inital release using AREL
44
+ - initial release using AREL
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in acts_as_recursive_tree.gemspec
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
- begin
3
- require 'rspec/core/rake_task'
4
- RSpec::Core::RakeTask.new(:spec)
4
+ require 'rspec/core/rake_task'
5
5
 
6
- task default: [:spec]
7
- rescue LoadError
8
- end
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( db.log test.sqlite3 ).each do |file|
13
- File.delete(file) if File.exists?(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
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 = 'acts_as_recursive_tree'
8
- spec.version = ActsAsRecursiveTree::VERSION
9
- spec.authors = ['Wolfgang Wedelich-John']
10
- spec.email = ['wolfgang.wedelich@1und1.de']
11
- spec.summary = %q{Drop in replacement for acts_as_tree but using recursive queries}
12
- spec.description = %q{
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 = 'https://github.com/1and1/acts_as_recursive_tree'
15
- spec.license = 'MIT'
16
-
17
- spec.required_ruby_version = '>= 2.0.0'
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.0.0', '< 6.0.0'
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 'bundler', '~> 1.7'
30
+ spec.add_development_dependency 'appraisal', '~> 2.4'
26
31
  spec.add_development_dependency 'database_cleaner', '~> 1.5'
27
- spec.add_development_dependency 'rake', '~> 10.0'
28
- spec.add_development_dependency 'rspec-rails', '~> 3.5'
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
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.2.0"
6
+ gem "activesupport", "~> 5.2.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.0.0"
6
+ gem "activesupport", "~> 6.0.0"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1.0"
6
+ gem "activesupport", "~> 6.1.0"
7
+
8
+ gemspec path: "../"
@@ -1,15 +1,11 @@
1
- require 'active_support/all'
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative 'acts_as_recursive_tree/railtie' if defined?(Rails)
4
+ require 'zeitwerk'
3
5
 
4
- module ActsAsRecursiveTree
5
- extend ActiveSupport::Autoload
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
6
8
 
7
- autoload :Config
8
- autoload :ActsMacro
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: self,
14
- parent_key: parent_key.to_sym,
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
- alias_method :acts_as_tree, :recursive_tree
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: self.base_class.to_s,
10
- foreign_key: self._recursive_tree_config.parent_key,
11
- inverse_of: :children,
12
- optional: true
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: self.base_class.to_s,
16
- foreign_key: self._recursive_tree_config.parent_key,
17
- inverse_of: :parent
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: self.base_class.to_s
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
- def build_join_condition
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
- travers_loc_table[primary_key].not_in(
10
- travers_loc_table.where(
11
- travers_loc_table[parent_key].not_eq(nil)
12
- ).project(travers_loc_table[parent_key])
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.call(opts) if 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 = Strategy.for_query_options(@query_opts).build(self)
56
+ relation = Strategies.for_query_options(@query_opts).build(self)
53
57
 
54
- relation = apply_except_id(relation)
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(relation)
64
- return relation unless depth_present?
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
- def apply_order(relation)
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
- travers_loc_table[column]
77
- else
78
- Arel.star
79
- end
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
- travers_loc_table.project(projections).with(:recursive, build_cte_table)
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(build_join_condition)
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 unless parent_type_column.present?
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