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