activerecord-hierarchical_query 0.0.2 → 0.0.5

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -30
  3. data/lib/active_record/hierarchical_query.rb +11 -11
  4. data/lib/active_record/hierarchical_query/cte/columns.rb +2 -15
  5. data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +54 -0
  6. data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +32 -11
  7. data/lib/active_record/hierarchical_query/cte/query_builder.rb +82 -0
  8. data/lib/active_record/hierarchical_query/cte/recursive_term.rb +27 -13
  9. data/lib/active_record/hierarchical_query/cte/union_term.rb +10 -6
  10. data/lib/active_record/hierarchical_query/join_builder.rb +31 -10
  11. data/lib/active_record/hierarchical_query/orderings.rb +113 -0
  12. data/lib/active_record/hierarchical_query/{builder.rb → query.rb} +66 -36
  13. data/lib/active_record/hierarchical_query/version.rb +1 -1
  14. data/lib/arel/nodes/postgresql.rb +26 -6
  15. data/spec/active_record/hierarchical_query_spec.rb +56 -32
  16. data/spec/database.yml +1 -9
  17. data/spec/schema.rb +2 -2
  18. data/spec/spec_helper.rb +2 -4
  19. data/spec/support/models.rb +4 -4
  20. metadata +28 -33
  21. data/lib/active_record/hierarchical_query/adapters.rb +0 -34
  22. data/lib/active_record/hierarchical_query/adapters/abstract.rb +0 -41
  23. data/lib/active_record/hierarchical_query/adapters/postgresql.rb +0 -19
  24. data/lib/active_record/hierarchical_query/cte/query.rb +0 -65
  25. data/lib/active_record/hierarchical_query/visitors/orderings.rb +0 -87
  26. data/lib/active_record/hierarchical_query/visitors/postgresql/cycle_detector.rb +0 -49
  27. data/lib/active_record/hierarchical_query/visitors/postgresql/orderings.rb +0 -70
  28. data/lib/active_record/hierarchical_query/visitors/visitor.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e317315d0b567e0880277e328380478df97fecd7
4
- data.tar.gz: f1918a2d4c9a9e8a84f59b7af8fec42d6fb57dfa
3
+ metadata.gz: 76f5e82aca51ebd96a9cc00a729b5f418e6fa385
4
+ data.tar.gz: 3ea6fa61b8b16e6ca0e18948d9280c70417a6386
5
5
  SHA512:
6
- metadata.gz: aad9047098ff944791fd13f5fed4a120e39195d3f9f4d08a17f5e734dfc8e7409e2a034acec7e0200df336c4a530003ebca1d68c2a494fac9629db3d253ace5f
7
- data.tar.gz: 890c7ebfff9df375347f00c061ed213167fe8e09da1cf567996afc9ca5417f264215c270e987a9e8a16925284d0f9cbafc842c2f26d8c8fd142dfd29761e88d9
6
+ metadata.gz: 5af1b963cdc7763d04a445c1ef248c0ac53bef531b45b15a20cc7a3743e34291a8a0b92bd79786cfec44a60c64f517679f0da57520eeb630665a23d84832b8e6
7
+ data.tar.gz: f5c489b92a0422ac00dbaec5f6df28ef931e7b835c4a0b7a3f99d95a6e66e8c2616932f19b858b6772fc91f72a9d4b640b6a684c7dbdaee5daeb347202a98301
data/README.md CHANGED
@@ -10,29 +10,62 @@ Create hierarchical queries using simple DSL, recursively traverse trees using s
10
10
 
11
11
  If a table contains hierarchical data, then you can select rows in hierarchical order using hierarchical query builder.
12
12
 
13
+ ### Traverse trees
14
+
15
+ Let's say you've got an ActiveRecord model `Category` that related to itself:
16
+
17
+ ```ruby
18
+ class Category < ActiveRecord::Base
19
+ belongs_to :parent, class_name: 'Category'
20
+ has_many :children, foreign_key: :parent_id, class_name: 'Category'
21
+ end
22
+
23
+ # Table definition
24
+ # create_table :categories do |t|
25
+ # t.integer :parent_id
26
+ # t.string :name
27
+ # end
28
+ ```
13
29
 
14
30
  ### Traverse descendants
15
31
 
16
32
  ```ruby
17
33
  Category.join_recursive do |query|
18
- query.start_with(:parent_id => nil)
19
- .connect_by(:id => :parent_id)
34
+ query.start_with(parent_id: nil)
35
+ .connect_by(id: :parent_id)
20
36
  .order_siblings(:name)
21
- end
37
+ end # returns ActiveRecord::Relation instance
22
38
  ```
23
39
 
24
40
  ### Traverse ancestors
25
41
 
26
42
  ```ruby
27
43
  Category.join_recursive do |query|
28
- query.start_with(:id => 42)
29
- .connect_by(:parent_id => :id)
44
+ query.start_with(id: 42)
45
+ .connect_by(parent_id: :id)
30
46
  end
31
47
  ```
32
48
 
49
+ ### Show breadcrumbs using single SQL query
50
+
51
+ ```ruby
52
+ records = Category.join_recursive do |query|
53
+ query
54
+ # assume that deepest node has depth=0
55
+ .start_with(id: 42) { select('0 depth') }
56
+ # for each ancestor decrease depth by 1, do not apply
57
+ # following expression to first level of hierarchy
58
+ .select(query.prior[:depth] - 1, start_with: false)
59
+ .connect_by(parent_id: :id)
60
+ end.order('depth ASC')
61
+
62
+ # returned value is just regular ActiveRecord::Relation instance, so you can use its methods
63
+ crumbs = records.pluck(:name).join(' / ')
64
+ ```
65
+
33
66
  ## Requirements
34
67
 
35
- * ActiveRecord >= 3.1.0
68
+ * ActiveRecord >= 3.1.0 (Rails 4.2 included)
36
69
  * PostgreSQL >= 8.4
37
70
 
38
71
  ## Installation
@@ -57,8 +90,8 @@ and `name`. You can traverse nodes recursively starting from root rows connected
57
90
 
58
91
  ```ruby
59
92
  Category.join_recursive do
60
- start_with(:parent_id => nil).
61
- connect_by(:id => :parent_id).
93
+ start_with(parent_id: nil).
94
+ connect_by(id: :parent_id).
62
95
  order_siblings(:name)
63
96
  end
64
97
  ```
@@ -81,10 +114,10 @@ Hierarchical queries are processed as follows:
81
114
 
82
115
  * First, root rows are selected -- those rows that satisfy `START WITH` condition in
83
116
  order specified by `ORDER SIBLINGS` clause. In example above it's specified by
84
- statements `query.start_with(:parent_id => nil)` and `query.order_siblings(:name)`.
117
+ statements `query.start_with(parent_id: nil)` and `query.order_siblings(:name)`.
85
118
  * Second, child rows for each root rows are selected. Each child row must satisfy
86
119
  condition specified by `CONNECT BY` clause with respect to one of the root rows
87
- (`query.connect_by(:id => :parent_id)` in example above). Order of child rows is
120
+ (`query.connect_by(id: :parent_id)` in example above). Order of child rows is
88
121
  also specified by `ORDER SIBLINGS` clause.
89
122
  * Successive generations of child rows are selected with respect to `CONNECT BY` clause.
90
123
  First the children of each row selected in step 2 selected, then the children of those
@@ -95,9 +128,9 @@ Hierarchical queries are processed as follows:
95
128
  This clause is specified by `start_with` method:
96
129
 
97
130
  ```ruby
98
- Category.join_recursive { start_with(:parent_id => nil) }
99
- Category.join_recursive { start_with { where(:parent_id => nil) } }
100
- Category.join_recursive { start_with { |root_rows| root_rows.where(:parent_id => nil) } }
131
+ Category.join_recursive { start_with(parent_id: nil) }
132
+ Category.join_recursive { start_with { where(parent_id: nil) } }
133
+ Category.join_recursive { start_with { |root_rows| root_rows.where(parent_id: nil) } }
101
134
  ```
102
135
 
103
136
  All of these statements are equivalent.
@@ -108,7 +141,7 @@ This clause is necessary and specified by `connect_by` method:
108
141
 
109
142
  ```ruby
110
143
  # join parent table ID columns and child table PARENT_ID column
111
- Category.join_recursive { connect_by(:id => :parent_id) }
144
+ Category.join_recursive { connect_by(id: :parent_id) }
112
145
 
113
146
  # you can use block to build complex JOIN conditions
114
147
  Category.join_recursive do
@@ -126,7 +159,7 @@ You can specify order in which rows on each hierarchy level should appear:
126
159
  Category.join_recursive { order_siblings(:name) }
127
160
 
128
161
  # you can reverse order
129
- Category.join_recursive { order_siblings(:name => :desc) }
162
+ Category.join_recursive { order_siblings(name: :desc) }
130
163
 
131
164
  # arbitrary strings and Arel nodes are allowed also
132
165
  Category.join_recursive { order_siblings('name ASC') }
@@ -139,7 +172,7 @@ You can filter rows on each hierarchy level by applying `WHERE` conditions:
139
172
 
140
173
  ```ruby
141
174
  Category.join_recursive do
142
- connect_by(:id => :parent_id).where('name LIKE ?', 'ruby %')
175
+ connect_by(id: :parent_id).where('name LIKE ?', 'ruby %')
143
176
  end
144
177
  ```
145
178
 
@@ -147,7 +180,7 @@ You can even refer to parent table, just don't forget to include columns in `SEL
147
180
 
148
181
  ```ruby
149
182
  Category.join_recursive do |query|
150
- query.connect_by(:id => :parent_id)
183
+ query.connect_by(id: :parent_id)
151
184
  .select(:name).
152
185
  .where(query.prior[:name].matches('ruby %'))
153
186
  end
@@ -157,7 +190,7 @@ Or, if Arel semantics does not fit your needs:
157
190
 
158
191
  ```ruby
159
192
  Category.join_recursive do |query|
160
- query.connect_by(:id => :parent_id)
193
+ query.connect_by(id: :parent_id)
161
194
  .where("#{query.prior.name}.name LIKE ?", 'ruby %')
162
195
  end
163
196
  ```
@@ -171,7 +204,7 @@ Loop example:
171
204
 
172
205
  ```ruby
173
206
  node_1 = Category.create
174
- node_2 = Category.create(:parent => node_1)
207
+ node_2 = Category.create(parent: node_1)
175
208
 
176
209
  node_1.parent = node_2
177
210
  node_1.save
@@ -181,8 +214,8 @@ node_1.save
181
214
 
182
215
  ```ruby
183
216
  Category.join_recursive do |query|
184
- query.connect_by(:id => :parent_id)
185
- .start_with(:id => node_1.id)
217
+ query.connect_by(id: :parent_id)
218
+ .start_with(id: node_1.id)
186
219
  end
187
220
  ```
188
221
 
@@ -190,8 +223,8 @@ end
190
223
 
191
224
  ```ruby
192
225
  Category.join_recursive do |query|
193
- query.connect_by(:id => :parent_id)
194
- .start_with(:id => node_1.id)
226
+ query.connect_by(id: :parent_id)
227
+ .start_with(id: node_1.id)
195
228
  .nocycle
196
229
  end
197
230
  ```
@@ -204,10 +237,10 @@ For example, this piece of code
204
237
 
205
238
  ```ruby
206
239
  Category.join_recursive do |query|
207
- query.start_with(:parent_id => nil) { select('0 LEVEL') }
208
- .connect_by(:id => :parent_id)
240
+ query.start_with(parent_id: nil) { select('0 LEVEL') }
241
+ .connect_by(id: :parent_id)
209
242
  .select(:depth)
210
- .select(query.prior[:LEVEL] + 1, :start_with => false)
243
+ .select(query.prior[:LEVEL] + 1, start_with: false)
211
244
  .where(query.prior[:depth].lteq(5))
212
245
  .order_siblings(:position)
213
246
  .nocycle
@@ -247,10 +280,6 @@ FROM "categories" INNER JOIN (
247
280
  ORDER BY "categories__recursive"."__order_column" ASC
248
281
  ```
249
282
 
250
- ## Future plans
251
-
252
- * Oracle support
253
-
254
283
  ## Related resources
255
284
 
256
285
  * [About hierarchical queries (Wikipedia)](http://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL)
@@ -3,7 +3,7 @@
3
3
  require 'active_support/lazy_load_hooks'
4
4
 
5
5
  require 'active_record/hierarchical_query/version'
6
- require 'active_record/hierarchical_query/builder'
6
+ require 'active_record/hierarchical_query/query'
7
7
  require 'active_record/version'
8
8
 
9
9
  module ActiveRecord
@@ -16,37 +16,37 @@ module ActiveRecord
16
16
  #
17
17
  # @example
18
18
  # MyModel.join_recursive do |query|
19
- # query.start_with(:parent_id => nil)
20
- # .connect_by(:id => :parent_id)
19
+ # query.start_with(parent_id: nil)
20
+ # .connect_by(id: :parent_id)
21
21
  # .where('depth < ?', 5)
22
- # .order_siblings(:name => :desc)
22
+ # .order_siblings(name: :desc)
23
23
  # end
24
24
  #
25
25
  # @param [Hash] join_options
26
26
  # @option join_options [String, Symbol] :as aliased name of joined
27
27
  # table (`%table_name%__recursive` by default)
28
28
  # @yield [query]
29
- # @yieldparam [ActiveRecord::HierarchicalQuery::Builder] query Hierarchical query builder
29
+ # @yieldparam [ActiveRecord::HierarchicalQuery::Query] query Hierarchical query
30
30
  # @raise [ArgumentError] if block is omitted
31
31
  def join_recursive(join_options = {}, &block)
32
32
  raise ArgumentError, 'block expected' unless block_given?
33
33
 
34
- builder = Builder.new(klass)
34
+ query = Query.new(klass)
35
35
 
36
36
  if block.arity == 0
37
- builder.instance_eval(&block)
37
+ query.instance_eval(&block)
38
38
  else
39
- block.call(builder)
39
+ block.call(query)
40
40
  end
41
41
 
42
- builder.join_to(self, join_options)
42
+ query.join_to(self, join_options)
43
43
  end
44
44
  end
45
45
  end
46
46
 
47
- ActiveSupport.on_load(:active_record, :yield => true) do |base|
47
+ ActiveSupport.on_load(:active_record, yield: true) do |base|
48
48
  class << base
49
- delegate :join_recursive, :to => ActiveRecord::HierarchicalQuery::DELEGATOR_SCOPE
49
+ delegate :join_recursive, to: ActiveRecord::HierarchicalQuery::DELEGATOR_SCOPE
50
50
  end
51
51
 
52
52
  ActiveRecord::Relation.send :include, ActiveRecord::HierarchicalQuery
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  module HierarchicalQuery
5
5
  module CTE
6
6
  class Columns
7
- # @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
7
+ # @param [ActiveRecord::HierarchicalQuery::Query] query
8
8
  def initialize(query)
9
9
  @query = query
10
10
  end
@@ -18,20 +18,7 @@ module ActiveRecord
18
18
 
19
19
  private
20
20
  def connect_by_columns
21
- extractor.call(@query.join_conditions).map { |column| column.name.to_s }
22
- end
23
-
24
- def extractor
25
- target = []
26
-
27
- ->(arel) {
28
- visitor = Arel::Visitors::DepthFirst.new do |node|
29
- target << node if node.is_a?(Arel::Attributes::Attribute)
30
- end
31
- visitor.accept(arel)
32
-
33
- target
34
- }
21
+ @query.join_conditions.grep(Arel::Attributes::Attribute) { |column| column.name.to_s }
35
22
  end
36
23
  end
37
24
  end
@@ -0,0 +1,54 @@
1
+ module ActiveRecord
2
+ module HierarchicalQuery
3
+ module CTE
4
+ class CycleDetector
5
+ COLUMN_NAME = '__path'.freeze
6
+
7
+ delegate :klass, :table, to: :@query
8
+
9
+ # @param [ActiveRecord::HierarchicalQuery::Query] query
10
+ def initialize(query)
11
+ @query = query
12
+ end
13
+
14
+ def apply_to_non_recursive(arel)
15
+ if enabled?
16
+ arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
17
+ end
18
+
19
+ arel
20
+ end
21
+
22
+ def apply_to_recursive(arel)
23
+ if enabled?
24
+ arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
25
+ arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
26
+ end
27
+
28
+ arel
29
+ end
30
+
31
+ private
32
+ def enabled?
33
+ @query.nocycle_value
34
+ end
35
+
36
+ def column_name
37
+ COLUMN_NAME
38
+ end
39
+
40
+ def parent_column
41
+ @query.recursive_table[column_name]
42
+ end
43
+
44
+ def primary_key
45
+ table[klass.primary_key]
46
+ end
47
+
48
+ def any(argument)
49
+ Arel::Nodes::NamedFunction.new('ANY', [argument])
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -6,26 +6,47 @@ module ActiveRecord
6
6
  class NonRecursiveTerm
7
7
  DISALLOWED_CLAUSES = :order, :limit, :offset, :group, :having
8
8
 
9
- attr_reader :query
10
- delegate :builder, :adapter, :to => :query
11
- delegate :start_with_value, :klass, :to => :builder
9
+ attr_reader :builder
10
+ delegate :query, to: :builder
12
11
 
13
- # @param [ActiveRecord::HierarchicalQuery::CTE::Query] query
14
- def initialize(query)
15
- @query = query
12
+ # @param [ActiveRecord::HierarchicalQuery::CTE::QueryBuilder] builder
13
+ def initialize(builder)
14
+ @builder = builder
15
+ end
16
+
17
+ def bind_values
18
+ scope.bind_values
16
19
  end
17
20
 
18
21
  def arel
19
- arel = scope.select(query.columns)
20
- .except(*DISALLOWED_CLAUSES)
21
- .arel
22
+ arel = scope.arel
22
23
 
23
- adapter.visit(:non_recursive, arel)
24
+ builder.cycle_detector.apply_to_non_recursive(arel)
24
25
  end
25
26
 
26
27
  private
27
28
  def scope
28
- start_with_value || klass.__send__(HierarchicalQuery::DELEGATOR_SCOPE)
29
+ @scope ||= query.
30
+ start_with_value.
31
+ select(columns).
32
+ except(*DISALLOWED_CLAUSES)
33
+ end
34
+
35
+ def columns
36
+ columns = builder.columns.to_a
37
+
38
+ if query.orderings.any?
39
+ columns << ordering
40
+ end
41
+
42
+ columns
43
+ end
44
+
45
+ def ordering
46
+ value = query.orderings.row_number_expression
47
+ column_name = query.ordering_column_name
48
+
49
+ Arel::Nodes::PostgresArray.new([value]).as(column_name)
29
50
  end
30
51
  end # class NonRecursiveTerm
31
52
  end
@@ -0,0 +1,82 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_record/hierarchical_query/cte/columns'
4
+ require 'active_record/hierarchical_query/cte/cycle_detector'
5
+ require 'active_record/hierarchical_query/cte/union_term'
6
+
7
+ module ActiveRecord
8
+ module HierarchicalQuery
9
+ module CTE
10
+ # CTE query builder
11
+ class QueryBuilder
12
+ attr_reader :query,
13
+ :columns,
14
+ :cycle_detector
15
+
16
+ delegate :klass, :table, :recursive_table, to: :query
17
+
18
+ # @param [ActiveRecord::HierarchicalQuery::Query] query
19
+ def initialize(query)
20
+ @query = query
21
+ @columns = Columns.new(@query)
22
+ @cycle_detector = CycleDetector.new(@query)
23
+ end
24
+
25
+ def bind_values
26
+ union_term.bind_values
27
+ end
28
+
29
+ # @return [Arel::SelectManager]
30
+ def build_arel
31
+ build_manager
32
+ build_select
33
+ build_limits
34
+ build_order
35
+
36
+ @arel
37
+ end
38
+
39
+ private
40
+ def build_manager
41
+ @arel = Arel::SelectManager.new(table.engine).
42
+ with(:recursive, with_query).
43
+ from(recursive_table)
44
+ end
45
+
46
+ # "categories__recursive" AS (
47
+ # SELECT ... FROM "categories"
48
+ # UNION ALL
49
+ # SELECT ... FROM "categories"
50
+ # INNER JOIN "categories__recursive" ON ...
51
+ # )
52
+ def with_query
53
+ Arel::Nodes::As.new(recursive_table, union_term.arel)
54
+ end
55
+
56
+ def union_term
57
+ @union_term ||= UnionTerm.new(self)
58
+ end
59
+
60
+ def build_select
61
+ @arel.project(recursive_table[Arel.star])
62
+ end
63
+
64
+ def build_limits
65
+ @arel.take(query.limit_value).skip(query.offset_value)
66
+ end
67
+
68
+ def build_order
69
+ @arel.order(order_column.asc) if should_order?
70
+ end
71
+
72
+ def should_order?
73
+ query.orderings.any? && (query.limit_value || query.offset_value)
74
+ end
75
+
76
+ def order_column
77
+ recursive_table[query.ordering_column_name]
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end