activerecord-hierarchical_query 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 031176cf523c47f50c3ca2a10b7c79129766b1e2
4
+ data.tar.gz: 9abc865a1f8e2bcbc42f084611b1d588ab7f8879
5
+ SHA512:
6
+ metadata.gz: e5a632144805142246f4ba00cdf938b785660e5bac28063194782efbd446d901fc9154c79c8ee0c039d2d61e7afe4b10a58ff62a84115941653f5943cc137059
7
+ data.tar.gz: 7931d4bf31d74b4759ab93d779efdac40ef0cdb10f3b1d583bc9120a48395eeb5c9409ee48790ff626303bf47a4129d924df98c7b2c0613f9b638696e4b5c5ea
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Alexei Mikhailov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # ActiveRecord::HierarchicalQuery
2
+
3
+ [![Build Status](https://travis-ci.org/take-five/activerecord-hierarchical_query.png?branch=master)](https://travis-ci.org/take-five/activerecord-hierarchical_query)
4
+ [![Code Climate](https://codeclimate.com/github/take-five/activerecord-hierarchical_query.png)](https://codeclimate.com/github/take-five/activerecord-hierarchical_query)
5
+ [![Coverage Status](https://coveralls.io/repos/take-five/activerecord-hierarchical_query/badge.png)](https://coveralls.io/r/take-five/activerecord-hierarchical_query)
6
+ [![Dependency Status](https://gemnasium.com/take-five/activerecord-hierarchical_query.png)](https://gemnasium.com/take-five/activerecord-hierarchical_query)
7
+
8
+ Create hierarchical queries using simple DSL, recursively traverse trees using single SQL query.
9
+
10
+ If a table contains hierarchical data, then you can select rows in hierarchical order using hierarchical query builder.
11
+
12
+
13
+ ### Traverse descendants
14
+
15
+ ```ruby
16
+ Category.join_recursive do |query|
17
+ query.start_with(:parent_id => nil)
18
+ .connect_by(:id => :parent_id)
19
+ .order_siblings(:name)
20
+ end
21
+ ```
22
+
23
+ ### Traverse ancestors
24
+
25
+ ```ruby
26
+ Category.join_recursive do |query|
27
+ query.start_with(:id => 42)
28
+ .connect_by(:parent_id => :id)
29
+ end
30
+ ```
31
+
32
+ ## Requirements
33
+
34
+ * ActiveRecord >= 3.1.0
35
+ * PostgreSQL >= 8.4
36
+
37
+ ## Installation
38
+
39
+ Add this line to your application's Gemfile:
40
+
41
+ gem 'activerecord-hierarchical_query'
42
+
43
+ And then execute:
44
+
45
+ $ bundle
46
+
47
+ Or install it yourself as:
48
+
49
+ $ gem install activerecord-hierarchical_query
50
+
51
+ ## Usage
52
+
53
+ Let's say you've got an ActiveRecord model `Category` with attributes `id`, `parent_id`
54
+ and `name`. You can traverse nodes recursively starting from root rows connected by
55
+ `parent_id` column ordered by `name`:
56
+
57
+ ```ruby
58
+ Category.join_recursive do
59
+ start_with(:parent_id => nil).
60
+ connect_by(:id => :parent_id).
61
+ order_siblings(:name)
62
+ end
63
+ ```
64
+
65
+ Hierarchical queries consist of these important clauses:
66
+
67
+ * **START WITH** clause
68
+
69
+ This clause specifies the root row(s) of the hierarchy.
70
+ * **CONNECT BY** clause
71
+
72
+ This clause specifies relationship between parent rows and child rows of the hierarchy.
73
+ * **ORDER SIBLINGS** clause
74
+
75
+ This clause specifies an order of rows in which they appear on each hierarchy level.
76
+
77
+ These terms are borrowed from [Oracle hierarchical queries syntax](http://docs.oracle.com/cd/B19306_01/server.102/b14200/queries003.htm).
78
+
79
+ Hierarchical queries are processed as follows:
80
+
81
+ * First, root rows are selected -- those rows that satisfy `START WITH` condition in
82
+ order specified by `ORDER SIBLINGS` clause. In example above it's specified by
83
+ statements `query.start_with(:parent_id => nil)` and `query.order_siblings(:name)`.
84
+ * Second, child rows for each root rows are selected. Each child row must satisfy
85
+ condition specified by `CONNECT BY` clause with respect to one of the root rows
86
+ (`query.connect_by(:id => :parent_id)` in example above). Order of child rows is
87
+ also specified by `ORDER SIBLINGS` clause.
88
+ * Successive generations of child rows are selected with respect to `CONNECT BY` clause.
89
+ First the children of each row selected in step 2 selected, then the children of those
90
+ children and so on.
91
+
92
+ ### START WITH
93
+
94
+ This clause is specified by `start_with` method:
95
+
96
+ ```ruby
97
+ Category.join_recursive { start_with(:parent_id => nil) }
98
+ Category.join_recursive { start_with { where(:parent_id => nil) } }
99
+ Category.join_recursive { start_with { |root_rows| root_rows.where(:parent_id => nil) } }
100
+ ```
101
+
102
+ All of these statements are equivalent.
103
+
104
+ ### CONNECT BY
105
+
106
+ This clause is necessary and specified by `connect_by` method:
107
+
108
+ ```ruby
109
+ # join parent table ID columns and child table PARENT_ID column
110
+ Category.join_recursive { connect_by(:id => :parent_id) }
111
+
112
+ # you can use block to build complex JOIN conditions
113
+ Category.join_recursive do
114
+ connect_by do |parent_table, child_table|
115
+ parent_table[:id].eq child_table[:parent_id]
116
+ end
117
+ end
118
+ ```
119
+
120
+ ### ORDER SIBLINGS
121
+
122
+ You can specify order in which rows on each hierarchy level should appear:
123
+
124
+ ```ruby
125
+ Category.join_recursive { order_siblings(:name) }
126
+
127
+ # you can reverse order
128
+ Category.join_recursive { order_siblings(:name => :desc) }
129
+
130
+ # arbitrary strings and Arel nodes are allowed also
131
+ Category.join_recursive { order_siblings('name ASC') }
132
+ Category.join_recursive { |query| query.order_siblings(query.table[:name].asc) }
133
+ ```
134
+
135
+ ### WHERE conditions
136
+
137
+ You can filter rows on each hierarchy level by applying `WHERE` conditions:
138
+
139
+ ```ruby
140
+ Category.join_recursive do
141
+ connect_by(:id => :parent_id).where('name LIKE ?', 'ruby %')
142
+ end
143
+ ```
144
+
145
+ You can even refer to parent table, just don't forget to include columns in `SELECT` clause!
146
+
147
+ ```ruby
148
+ Category.join_recursive do |query|
149
+ query.connect_by(:id => :parent_id)
150
+ .select(:name).
151
+ .where(query.prior[:name].matches('ruby %'))
152
+ end
153
+ ```
154
+
155
+ Or, if Arel semantics does not fit your needs:
156
+
157
+ ```ruby
158
+ Category.join_recursive do |query|
159
+ query.connect_by(:id => :parent_id)
160
+ .where("#{query.prior.name}.name LIKE ?", 'ruby %')
161
+ end
162
+ ```
163
+
164
+ ### NOCYCLE
165
+
166
+ Recursive query will loop if hierarchy contains cycles (your graph is not acyclic).
167
+ `NOCYCLE` clause, which is turned off by default, could prevent it.
168
+
169
+ Loop example:
170
+
171
+ ```ruby
172
+ node_1 = Category.create
173
+ node_2 = Category.create(:parent => node_1)
174
+
175
+ node_1.parent = node_2
176
+ node_1.save
177
+ ```
178
+
179
+ `node_1` and `node_2` now link to each other, so following query will never end:
180
+
181
+ ```ruby
182
+ Category.join_recursive do |query|
183
+ query.connect_by(:id => :parent_id)
184
+ .start_with(:id => node_1.id)
185
+ end
186
+ ```
187
+
188
+ `#nocycle` method will prevent endless loop:
189
+
190
+ ```ruby
191
+ Category.join_recursive do |query|
192
+ query.connect_by(:id => :parent_id)
193
+ .start_with(:id => node_1.id)
194
+ .nocycle
195
+ end
196
+ ```
197
+
198
+ ## Generated SQL queries
199
+
200
+ Under the hood this extensions builds `INNER JOIN` to recursive subquery.
201
+
202
+ For example, this piece of code
203
+
204
+ ```ruby
205
+ Category.join_recursive do |query|
206
+ query.start_with(:parent_id => nil) { select('0 LEVEL') }
207
+ .connect_by(:id => :parent_id)
208
+ .select(:depth)
209
+ .select(query.prior[:LEVEL] + 1, :start_with => false)
210
+ .where(query.prior[:depth].lteq(5))
211
+ .order_siblings(:position)
212
+ .nocycle
213
+ end
214
+ ```
215
+
216
+ would generate following SQL (if PostgreSQL used):
217
+
218
+ ```sql
219
+ SELECT "categories".*
220
+ FROM "categories" INNER JOIN (
221
+ WITH RECURSIVE "categories__recursive" AS (
222
+ SELECT depth,
223
+ 0 LEVEL,
224
+ "categories"."id",
225
+ "categories"."parent_id",
226
+ ARRAY["categories"."position"] AS __order_column,
227
+ ARRAY["categories"."id"] AS __path
228
+ FROM "categories"
229
+ WHERE "categories"."parent_id" IS NULL
230
+
231
+ UNION ALL
232
+
233
+ SELECT "categories"."depth",
234
+ "categories__recursive"."LEVEL" + 1,
235
+ "categories"."id",
236
+ "categories"."parent_id",
237
+ "categories__recursive"."__order_column" || "categories"."position",
238
+ "categories__recursive"."__path" || "categories"."id"
239
+ FROM "categories" INNER JOIN
240
+ "categories__recursive" ON "categories__recursive"."id" = "categories"."parent_id"
241
+ WHERE ("categories__recursive"."depth" <= 5) AND
242
+ NOT ("categories"."id" = ANY("categories__recursive"."__path"))
243
+ )
244
+ SELECT "categories__recursive".* FROM "categories__recursive"
245
+ ) AS "categories__recursive" ON "categories"."id" = "categories__recursive"."id"
246
+ ORDER BY "categories__recursive"."__order_column" ASC
247
+ ```
248
+
249
+ ## Future plans
250
+
251
+ * Oracle support
252
+
253
+ ## Related resources
254
+
255
+ * [About hierarchical queries (Wikipedia)](http://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL)
256
+ * [Hierarchical queries in Oracle](http://docs.oracle.com/cd/B19306_01/server.102/b14200/queries003.htm)
257
+ * [Recursive queries in PostgreSQL](http://www.postgresql.org/docs/9.3/static/queries-with.html)
258
+ * [Using Recursive SQL with ActiveRecord trees](http://hashrocket.com/blog/posts/recursive-sql-in-activerecord)
259
+
260
+ ## Contributing
261
+
262
+ 1. Fork it ( http://github.com/take-five/activerecord-hierarchical_query/fork )
263
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
264
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
265
+ 4. Push to the branch (`git push origin my-new-feature`)
266
+ 5. Create new Pull Request
@@ -0,0 +1,53 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ require 'active_record/hierarchical_query/version'
6
+ require 'active_record/hierarchical_query/builder'
7
+ require 'active_record/version'
8
+
9
+ module ActiveRecord
10
+ module HierarchicalQuery
11
+ # @api private
12
+ DELEGATOR_SCOPE = ActiveRecord::VERSION::STRING < '4.0.0' ? :scoped : :all
13
+
14
+ # Performs a join to recursive subquery
15
+ # which should be built within a block.
16
+ #
17
+ # @example
18
+ # MyModel.join_recursive do |query|
19
+ # query.start_with(:parent_id => nil)
20
+ # .connect_by(:id => :parent_id)
21
+ # .where('depth < ?', 5)
22
+ # .order_siblings(:name => :desc)
23
+ # end
24
+ #
25
+ # @param [Hash] join_options
26
+ # @option join_options [String, Symbol] :as aliased name of joined
27
+ # table (`%table_name%__recursive` by default)
28
+ # @yield [query]
29
+ # @yieldparam [ActiveRecord::HierarchicalQuery::Builder] query Hierarchical query builder
30
+ # @raise [ArgumentError] if block is omitted
31
+ def join_recursive(join_options = {}, &block)
32
+ raise ArgumentError, 'block expected' unless block_given?
33
+
34
+ builder = Builder.new(klass)
35
+
36
+ if block.arity == 0
37
+ builder.instance_eval(&block)
38
+ else
39
+ block.call(builder)
40
+ end
41
+
42
+ builder.join_to(self, join_options)
43
+ end
44
+ end
45
+ end
46
+
47
+ ActiveSupport.on_load(:active_record, :yield => true) do |base|
48
+ class << base
49
+ delegate :join_recursive, :to => ActiveRecord::HierarchicalQuery::DELEGATOR_SCOPE
50
+ end
51
+
52
+ ActiveRecord::Relation.send :include, ActiveRecord::HierarchicalQuery
53
+ end
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+
3
+ module ActiveRecord
4
+ module HierarchicalQuery
5
+ module Adapters
6
+ SUPPORTED_ADAPTERS = %w(PostgreSQL)
7
+
8
+ autoload :PostgreSQL, 'active_record/hierarchical_query/adapters/postgresql'
9
+
10
+ def self.lookup(klass)
11
+ name = klass.connection.adapter_name
12
+
13
+ raise 'Your database does not support recursive queries' unless
14
+ SUPPORTED_ADAPTERS.include?(name)
15
+
16
+ const_get(name)
17
+ end
18
+ end # module Adapters
19
+ end # module HierarchicalQuery
20
+ end # module ActiveRecord
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_record/hierarchical_query/cte/query'
4
+
5
+ module ActiveRecord
6
+ module HierarchicalQuery
7
+ module Adapters
8
+ # @api private
9
+ class PostgreSQL
10
+ attr_reader :builder,
11
+ :table
12
+
13
+ delegate :klass, :to => :builder
14
+ delegate :build_join, :to => :@query
15
+
16
+ # @param [ActiveRecord::HierarchicalQuery::Builder] builder
17
+ def initialize(builder)
18
+ @builder = builder
19
+ @table = klass.arel_table
20
+ @query = CTE::Query.new(builder)
21
+ end
22
+
23
+ def prior
24
+ @query.recursive_table
25
+ end
26
+ end # class PostgreSQL
27
+ end # module Adapters
28
+ end # module HierarchicalQuery
29
+ end # module ActiveRecord
@@ -0,0 +1,289 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/core_ext/array/extract_options'
4
+
5
+ require 'active_record/hierarchical_query/adapters'
6
+
7
+ module ActiveRecord
8
+ module HierarchicalQuery
9
+ class Builder
10
+ # @api private
11
+ attr_reader :klass,
12
+ :start_with_value,
13
+ :connect_by_value,
14
+ :child_scope_value,
15
+ :limit_value,
16
+ :offset_value,
17
+ :order_values,
18
+ :nocycle_value
19
+
20
+ # @api private
21
+ CHILD_SCOPE_METHODS = :where, :joins, :group, :having
22
+
23
+ def initialize(klass)
24
+ @klass = klass
25
+ @adapter = Adapters.lookup(@klass).new(self)
26
+
27
+ @start_with_value = nil
28
+ @connect_by_value = nil
29
+ @child_scope_value = klass
30
+ @limit_value = nil
31
+ @offset_value = nil
32
+ @nocycle_value = false
33
+ @order_values = []
34
+ end
35
+
36
+ # Specify root scope of the hierarchy.
37
+ #
38
+ # @example When scope given
39
+ # MyModel.join_recursive do |hierarchy|
40
+ # hierarchy.start_with(MyModel.where(:parent_id => nil))
41
+ # .connect_by(:id => :parent_id)
42
+ # end
43
+ #
44
+ # @example When Hash given
45
+ # MyModel.join_recursive do |hierarchy|
46
+ # hierarchy.start_with(:parent_id => nil)
47
+ # .connect_by(:id => :parent_id)
48
+ # end
49
+ #
50
+ # @example When block given
51
+ # MyModel.join_recursive do |hierarchy|
52
+ # hierarchy.start_with { |root| root.where(:parent_id => nil) }
53
+ # .connect_by(:id => :parent_id)
54
+ # end
55
+ #
56
+ # @example When block with arity=0 given
57
+ # MyModel.join_recursive do |hierarchy|
58
+ # hierarchy.start_with { where(:parent_id => nil) }
59
+ # .connect_by(:id => :parent_id)
60
+ # end
61
+ #
62
+ # @example Specify columns for root relation (PostgreSQL-specific)
63
+ # MyModel.join_recursive do |hierarchy|
64
+ # hierarchy.start_with { select('ARRAY[id] AS _path') }
65
+ # .connect_by(:id => :parent_id)
66
+ # .select('_path || id', :start_with => false) # `:start_with => false` tells not to include this expression into START WITH clause
67
+ # end
68
+ #
69
+ # @param [ActiveRecord::Relation, Hash, nil] scope root scope (optional).
70
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
71
+ def start_with(scope = nil, &block)
72
+ raise ArgumentError, 'START WITH: scope or block expected, none given' unless scope || block
73
+
74
+ case scope
75
+ when Hash
76
+ @start_with_value = klass.where(scope)
77
+
78
+ when ActiveRecord::Relation
79
+ @start_with_value = scope
80
+
81
+ else
82
+ # do nothing if something weird given
83
+ end
84
+
85
+ if block
86
+ object = @start_with_value || @klass
87
+
88
+ @start_with_value = if block.arity == 0
89
+ object.instance_eval(&block)
90
+ else
91
+ block.call(object)
92
+ end
93
+ end
94
+
95
+ self
96
+ end
97
+
98
+ # Specify relationship between parent rows and child rows of the
99
+ # hierarchy. It can be specified with Hash where keys are parent columns
100
+ # names and values are child columns names, or with block (see example below).
101
+ #
102
+ # @example Specify relationship with Hash (traverse descendants)
103
+ # MyModel.join_recursive do |hierarchy|
104
+ # # join child rows with condition `parent.id = child.parent_id`
105
+ # hierarchy.connect_by(:id => :parent_id)
106
+ # end
107
+ #
108
+ # @example Specify relationship with block (traverse descendants)
109
+ # MyModel.join_recursive do |hierarchy|
110
+ # hierarchy.connect_by { |parent, child| parent[:id].eq(child[:parent_id]) }
111
+ # end
112
+ #
113
+ # @param [Hash, nil] conditions (optional) relationship between parent rows and
114
+ # child rows map, where keys are parent columns names and values are child columns names.
115
+ # @yield [parent, child] Yields both parent and child tables.
116
+ # @yieldparam [Arel::Table] parent parent rows table instance.
117
+ # @yieldparam [Arel::Table] child child rows table instance.
118
+ # @yieldreturn [Arel::Nodes::Node] relationship condition expressed as Arel node.
119
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
120
+ def connect_by(conditions = nil, &block)
121
+ # convert hash to block which returns Arel node
122
+ if conditions
123
+ block = conditions_to_proc(conditions)
124
+ end
125
+
126
+ raise ArgumentError, 'CONNECT BY: Conditions hash or block expected, none given' unless block
127
+
128
+ @connect_by_value = block
129
+
130
+ self
131
+ end
132
+
133
+ # Specify which columns should be selected in addition to primary key,
134
+ # CONNECT BY columns and ORDER SIBLINGS columns.
135
+ #
136
+ # @param [Array<Symbol, String, Arel::Attributes::Attribute, Arel::Nodes::Node>] columns
137
+ # @option columns [true, false] :start_with include given columns to START WITH clause (true by default)
138
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
139
+ def select(*columns)
140
+ options = columns.extract_options!
141
+
142
+ columns = columns.flatten.map do |column|
143
+ column.is_a?(Symbol) ? table[column] : column
144
+ end
145
+
146
+ # TODO: detect if column already present in START WITH clause and skip it
147
+ if options.fetch(:start_with, true)
148
+ start_with { |scope| scope.select(columns) }
149
+ end
150
+
151
+ @child_scope_value = @child_scope_value.select(columns)
152
+
153
+ self
154
+ end
155
+
156
+ # Generate methods that apply filters to child scope, such as
157
+ # +where+ or +group+.
158
+ #
159
+ # @example Filter child nodes by certain condition
160
+ # MyModel.join_recursive do |hierarchy|
161
+ # hierarchy.where('depth < 5')
162
+ # end
163
+ #
164
+ # @!method where(*conditions)
165
+ # @!method joins(*tables)
166
+ # @!method group(*values)
167
+ # @!method having(*conditions)
168
+ CHILD_SCOPE_METHODS.each do |method|
169
+ define_method(method) do |*args|
170
+ @child_scope_value = @child_scope_value.public_send(method, *args)
171
+
172
+ self
173
+ end
174
+ end
175
+
176
+ # Specifies a limit for the number of records to retrieve.
177
+ #
178
+ # @param [Fixnum] value
179
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
180
+ def limit(value)
181
+ @limit_value = value
182
+
183
+ self
184
+ end
185
+
186
+ # Specifies the number of rows to skip before returning row
187
+ #
188
+ # @param [Fixnum] value
189
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
190
+ def offset(value)
191
+ @offset_value = value
192
+
193
+ self
194
+ end
195
+
196
+ # Specifies hierarchical order of the recursive query results.
197
+ #
198
+ # @example
199
+ # MyModel.join_recursive do |hierarchy|
200
+ # hierarchy.connect_by(:id => :parent_id)
201
+ # .order_siblings(:name)
202
+ # end
203
+ #
204
+ # @example
205
+ # MyModel.join_recursive do |hierarchy|
206
+ # hierarchy.connect_by(:id => :parent_id)
207
+ # .order_siblings('name DESC, created_at ASC')
208
+ # end
209
+ #
210
+ # @param [<Symbol, String, Arel::Nodes::Node, Arel::Attributes::Attribute>] columns
211
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
212
+ def order_siblings(*columns)
213
+ @order_values += columns
214
+
215
+ self
216
+ end
217
+ alias_method :order, :order_siblings
218
+
219
+ # Turn on/off cycles detection. This option can prevent
220
+ # endless loops if your tree could contain cycles.
221
+ #
222
+ # @param [true, false] value
223
+ # @return [ActiveRecord::HierarchicalQuery::Builder] self
224
+ def nocycle(value = true)
225
+ @nocycle_value = value
226
+ self
227
+ end
228
+
229
+ # Returns object representing parent rows table,
230
+ # so it could be used in complex WHEREs.
231
+ #
232
+ # @example
233
+ # MyModel.join_recursive do |hierarchy|
234
+ # hierarchy.connect_by(:id => :parent_id)
235
+ # .start_with(:parent_id => nil) { select(:depth) }
236
+ # .select(hierarchy.table[:depth])
237
+ # .where(hierarchy.prior[:depth].lteq 1)
238
+ # end
239
+ #
240
+ # @return [Arel::Table]
241
+ def prior
242
+ @adapter.prior
243
+ end
244
+ alias_method :previous, :prior
245
+
246
+ # Returns object representing child rows table,
247
+ # so it could be used in complex WHEREs.
248
+ #
249
+ # @example
250
+ # MyModel.join_recursive do |hierarchy|
251
+ # hierarchy.connect_by(:id => :parent_id)
252
+ # .start_with(:parent_id => nil) { select(:depth) }
253
+ # .select(hierarchy.table[:depth])
254
+ # .where(hierarchy.prior[:depth].lteq 1)
255
+ # end
256
+ def table
257
+ @klass.arel_table
258
+ end
259
+
260
+ # Builds recursive query and joins it to given +relation+.
261
+ #
262
+ # @api private
263
+ # @param [ActiveRecord::Relation] relation
264
+ # @param [Hash] join_options
265
+ # @option join_options [#to_s] :as joined table alias
266
+ def join_to(relation, join_options = {})
267
+ raise 'Recursive query requires CONNECT BY clause, please use #connect_by method' unless
268
+ connect_by_value
269
+
270
+ table_alias = join_options.fetch(:as, "#{table.name}__recursive")
271
+
272
+ @adapter.build_join(relation, table_alias)
273
+ end
274
+
275
+ private
276
+ # converts conditions given as a hash to proc
277
+ def conditions_to_proc(conditions)
278
+ proc do |parent, child|
279
+ conditions.map do |parent_expression, child_expression|
280
+ parent_expression = parent[parent_expression] if parent_expression.is_a?(Symbol)
281
+ child_expression = child[child_expression] if child_expression.is_a?(Symbol)
282
+
283
+ Arel::Nodes::Equality.new(parent_expression, child_expression)
284
+ end.reduce(:and)
285
+ end
286
+ end
287
+ end # class Builder
288
+ end # module HierarchicalQuery
289
+ end # module ActiveRecord