activerecord-hierarchical_query 0.0.1

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