activerecord-hierarchical_query 0.0.2 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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