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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +266 -0
- data/lib/active_record/hierarchical_query.rb +53 -0
- data/lib/active_record/hierarchical_query/adapters.rb +20 -0
- data/lib/active_record/hierarchical_query/adapters/postgresql.rb +29 -0
- data/lib/active_record/hierarchical_query/builder.rb +289 -0
- data/lib/active_record/hierarchical_query/cte/columns.rb +39 -0
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +63 -0
- data/lib/active_record/hierarchical_query/cte/join_builder.rb +55 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +44 -0
- data/lib/active_record/hierarchical_query/cte/orderings.rb +108 -0
- data/lib/active_record/hierarchical_query/cte/query.rb +94 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +47 -0
- data/lib/active_record/hierarchical_query/cte/union_term.rb +28 -0
- data/lib/active_record/hierarchical_query/version.rb +5 -0
- data/lib/arel/nodes/postgresql.rb +28 -0
- data/spec/active_record/hierarchical_query_spec.rb +193 -0
- data/spec/database.travis.yml +4 -0
- data/spec/database.yml +4 -0
- data/spec/schema.rb +10 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/models.rb +39 -0
- metadata +171 -0
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
|
+
[](https://travis-ci.org/take-five/activerecord-hierarchical_query)
|
4
|
+
[](https://codeclimate.com/github/take-five/activerecord-hierarchical_query)
|
5
|
+
[](https://coveralls.io/r/take-five/activerecord-hierarchical_query)
|
6
|
+
[](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
|