acts_as_recursive_tree 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +1 -0
  5. data/.rubocop_todo.yml +321 -0
  6. data/CHANGELOG.md +17 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +175 -0
  10. data/Rakefile +15 -0
  11. data/acts_as_recursive_tree.gemspec +30 -0
  12. data/lib/acts_as_recursive_tree/acts_macro.rb +28 -0
  13. data/lib/acts_as_recursive_tree/associations.rb +25 -0
  14. data/lib/acts_as_recursive_tree/builders/ancestors.rb +17 -0
  15. data/lib/acts_as_recursive_tree/builders/descendants.rb +9 -0
  16. data/lib/acts_as_recursive_tree/builders/leaves.rb +26 -0
  17. data/lib/acts_as_recursive_tree/builders/relation_builder.rb +121 -0
  18. data/lib/acts_as_recursive_tree/builders.rb +13 -0
  19. data/lib/acts_as_recursive_tree/model.rb +125 -0
  20. data/lib/acts_as_recursive_tree/options/depth_condition.rb +48 -0
  21. data/lib/acts_as_recursive_tree/options/query_options.rb +21 -0
  22. data/lib/acts_as_recursive_tree/options/values.rb +77 -0
  23. data/lib/acts_as_recursive_tree/options.rb +9 -0
  24. data/lib/acts_as_recursive_tree/railtie.rb +9 -0
  25. data/lib/acts_as_recursive_tree/scopes.rb +16 -0
  26. data/lib/acts_as_recursive_tree/version.rb +3 -0
  27. data/lib/acts_as_recursive_tree.rb +14 -0
  28. data/spec/builders_spec.rb +136 -0
  29. data/spec/db/database.rb +22 -0
  30. data/spec/db/database.yml +12 -0
  31. data/spec/db/models.rb +37 -0
  32. data/spec/db/schema.rb +34 -0
  33. data/spec/model/location_spec.rb +55 -0
  34. data/spec/model/node_spec.rb +129 -0
  35. data/spec/model/relation_spec.rb +63 -0
  36. data/spec/spec_helper.rb +119 -0
  37. data/spec/values_spec.rb +86 -0
  38. 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,9 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ class Descendants < RelationBuilder
4
+ def build_join_condition
5
+ base_table[config.parent_key].eq(travers_loc_table[config.primary_key])
6
+ end
7
+ end
8
+ end
9
+ 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,9 @@
1
+ module ActsAsRecursiveTree
2
+ module Options
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Values
6
+ autoload :DepthCondition
7
+ autoload :QueryOptions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActsAsRecursiveTree
2
+ class Railtie < Rails::Railtie
3
+ initializer 'acts_as_recursive_tree.active_record_initializer' do
4
+ ActiveRecord::Base.class_exec do
5
+ extend ActsAsRecursiveTree::ActsMacro
6
+ end
7
+ end
8
+ end
9
+ 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,3 @@
1
+ module ActsAsRecursiveTree
2
+ VERSION = '2.0.0'.freeze
3
+ 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
@@ -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
+
@@ -0,0 +1,12 @@
1
+ common: &common
2
+ database:
3
+ host: localhost
4
+ pool: 50
5
+ timeout: 5000
6
+ reaping_frequency: 1000
7
+ min_messages: ERROR
8
+
9
+ sqlite:
10
+ <<: *common
11
+ adapter: sqlite3
12
+ database: test.sqlite3