cuatlan-activerecord-hierarchical_query 1.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 +356 -0
- data/lib/active_record/hierarchical_query.rb +53 -0
- data/lib/active_record/hierarchical_query/cte/columns.rb +26 -0
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +54 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +55 -0
- data/lib/active_record/hierarchical_query/cte/query_builder.rb +88 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +47 -0
- data/lib/active_record/hierarchical_query/cte/union_term.rb +35 -0
- data/lib/active_record/hierarchical_query/join_builder.rb +107 -0
- data/lib/active_record/hierarchical_query/orderings.rb +119 -0
- data/lib/active_record/hierarchical_query/query.rb +333 -0
- data/lib/active_record/hierarchical_query/version.rb +5 -0
- data/lib/activerecord-hierarchical_query.rb +1 -0
- data/lib/arel/nodes/postgresql.rb +60 -0
- data/spec/active_record/hierarchical_query_spec.rb +338 -0
- data/spec/database.travis.yml +5 -0
- data/spec/database.yml +8 -0
- data/spec/schema.rb +21 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/support/models.rb +50 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 12a45b3c8eccd84634a044029ebf6b5bf893de28f46a4ee00a74d8254a9eed88
|
4
|
+
data.tar.gz: c22d63e8514cea22bb2acb9e9f2ede158d17a1f62eb470a937943c9620f78639
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cf8660bcdace09d9b4cac59decf97bb955be87aef4a2363c908a087d075ee4c4d04a1fed2d240d8908fd25751428798f13adfad01eb5318b4f70acb87959f1b
|
7
|
+
data.tar.gz: cc4a27033c075bcc77b78c4a3743a81d6a99e78383ada23c039adef286e649d43aa15322f5e155e140d80a4ceaf3811c8df07d138d2c47b0a4910701cc029b1d
|
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,356 @@
|
|
1
|
+
# LOOKING FOR MAINTAINER
|
2
|
+
|
3
|
+
I'm sorry but I can't maintain this project anymore.
|
4
|
+
|
5
|
+
If you want to maintain this project, contact me (amikhailov83[at]gmail.com) and I will grant you all necessary permissions.
|
6
|
+
|
7
|
+
# ActiveRecord::HierarchicalQuery
|
8
|
+
|
9
|
+
[](https://travis-ci.org/take-five/activerecord-hierarchical_query)
|
10
|
+
[](https://codeclimate.com/github/take-five/activerecord-hierarchical_query)
|
11
|
+
[](https://coveralls.io/r/take-five/activerecord-hierarchical_query)
|
12
|
+
[](https://gemnasium.com/take-five/activerecord-hierarchical_query)
|
13
|
+
[](http://badge.fury.io/rb/activerecord-hierarchical_query)
|
14
|
+
|
15
|
+
Create hierarchical queries using simple DSL, recursively traverse trees using single SQL query.
|
16
|
+
|
17
|
+
If a table contains hierarchical data, then you can select rows in hierarchical order using hierarchical query builder.
|
18
|
+
|
19
|
+
### Traverse trees
|
20
|
+
|
21
|
+
Let's say you've got an ActiveRecord model `Category` that related to itself:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Category < ActiveRecord::Base
|
25
|
+
belongs_to :parent, class_name: 'Category'
|
26
|
+
has_many :children, foreign_key: :parent_id, class_name: 'Category'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Table definition
|
30
|
+
# create_table :categories do |t|
|
31
|
+
# t.integer :parent_id
|
32
|
+
# t.string :name
|
33
|
+
# end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Traverse descendants
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Category.join_recursive do |query|
|
40
|
+
query.start_with(parent_id: nil)
|
41
|
+
.connect_by(id: :parent_id)
|
42
|
+
.order_siblings(:name)
|
43
|
+
end # returns ActiveRecord::Relation instance
|
44
|
+
```
|
45
|
+
|
46
|
+
### Traverse ancestors
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
Category.join_recursive do |query|
|
50
|
+
query.start_with(id: 42)
|
51
|
+
.connect_by(parent_id: :id)
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### Show breadcrumbs using single SQL query
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
records = Category.join_recursive do |query|
|
59
|
+
query
|
60
|
+
# assume that deepest node has depth=0
|
61
|
+
.start_with(id: 42) { select('0 depth') }
|
62
|
+
# for each ancestor decrease depth by 1, do not apply
|
63
|
+
# following expression to first level of hierarchy
|
64
|
+
.select(query.prior[:depth] - 1, start_with: false)
|
65
|
+
.connect_by(parent_id: :id)
|
66
|
+
end.order('depth ASC')
|
67
|
+
|
68
|
+
# returned value is just regular ActiveRecord::Relation instance, so you can use its methods
|
69
|
+
crumbs = records.pluck(:name).join(' / ')
|
70
|
+
```
|
71
|
+
|
72
|
+
## Requirements
|
73
|
+
|
74
|
+
* ActiveRecord >= 3.1.0
|
75
|
+
* PostgreSQL >= 8.4
|
76
|
+
|
77
|
+
## Rails 5
|
78
|
+
|
79
|
+
Rails 5 is supported on the `rails-5` branch and through gem versions
|
80
|
+
`>= 1.0.0` on rubygems.org. The Rails branch is intended to
|
81
|
+
follow minor Rails releases (currently 5.1), but it should be
|
82
|
+
compatible with 5.0 as well. If you have trouble try upgrading Rails
|
83
|
+
first. Tag @zachaysan with in a GitHub issue if the latest version
|
84
|
+
of Rails is not supported or if there are reproducable problems on
|
85
|
+
the latest minor version of Rails 5.
|
86
|
+
|
87
|
+
## Installation
|
88
|
+
|
89
|
+
Add this line to your application's Gemfile:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
gem 'activerecord-hierarchical_query'
|
93
|
+
```
|
94
|
+
|
95
|
+
And then execute:
|
96
|
+
|
97
|
+
$ bundle
|
98
|
+
|
99
|
+
Or install it yourself as:
|
100
|
+
|
101
|
+
$ gem install activerecord-hierarchical_query
|
102
|
+
|
103
|
+
You'll then need to require the gem:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'active_record/hierarchical_query'
|
107
|
+
```
|
108
|
+
|
109
|
+
Alternatively, the require can be placed in the `Gemfile`:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
gem 'activerecord-hierarchical_query', require: 'active_record/hierarchical_query'
|
113
|
+
```
|
114
|
+
|
115
|
+
|
116
|
+
## Usage
|
117
|
+
|
118
|
+
Let's say you've got an ActiveRecord model `Category` with attributes `id`, `parent_id`
|
119
|
+
and `name`. You can traverse nodes recursively starting from root rows connected by
|
120
|
+
`parent_id` column ordered by `name`:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
Category.join_recursive do
|
124
|
+
start_with(parent_id: nil).
|
125
|
+
connect_by(id: :parent_id).
|
126
|
+
order_siblings(:name)
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
Hierarchical queries consist of these important clauses:
|
131
|
+
|
132
|
+
* **START WITH** clause
|
133
|
+
|
134
|
+
This clause specifies the root row(s) of the hierarchy.
|
135
|
+
* **CONNECT BY** clause
|
136
|
+
|
137
|
+
This clause specifies relationship between parent rows and child rows of the hierarchy.
|
138
|
+
* **ORDER SIBLINGS** clause
|
139
|
+
|
140
|
+
This clause specifies an order of rows in which they appear on each hierarchy level.
|
141
|
+
|
142
|
+
These terms are borrowed from [Oracle hierarchical queries syntax](http://docs.oracle.com/cd/B19306_01/server.102/b14200/queries003.htm).
|
143
|
+
|
144
|
+
Hierarchical queries are processed as follows:
|
145
|
+
|
146
|
+
* First, root rows are selected -- those rows that satisfy `START WITH` condition in
|
147
|
+
order specified by `ORDER SIBLINGS` clause. In example above it's specified by
|
148
|
+
statements `query.start_with(parent_id: nil)` and `query.order_siblings(:name)`.
|
149
|
+
* Second, child rows for each root rows are selected. Each child row must satisfy
|
150
|
+
condition specified by `CONNECT BY` clause with respect to one of the root rows
|
151
|
+
(`query.connect_by(id: :parent_id)` in example above). Order of child rows is
|
152
|
+
also specified by `ORDER SIBLINGS` clause.
|
153
|
+
* Successive generations of child rows are selected with respect to `CONNECT BY` clause.
|
154
|
+
First the children of each row selected in step 2 selected, then the children of those
|
155
|
+
children and so on.
|
156
|
+
|
157
|
+
### START WITH
|
158
|
+
|
159
|
+
This clause is specified by `start_with` method:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
Category.join_recursive { start_with(parent_id: nil) }
|
163
|
+
Category.join_recursive { start_with { where(parent_id: nil) } }
|
164
|
+
Category.join_recursive { start_with { |root_rows| root_rows.where(parent_id: nil) } }
|
165
|
+
```
|
166
|
+
|
167
|
+
All of these statements are equivalent.
|
168
|
+
|
169
|
+
### CONNECT BY
|
170
|
+
|
171
|
+
This clause is necessary and specified by `connect_by` method:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# join parent table ID columns and child table PARENT_ID column
|
175
|
+
Category.join_recursive { connect_by(id: :parent_id) }
|
176
|
+
|
177
|
+
# you can use block to build complex JOIN conditions
|
178
|
+
Category.join_recursive do
|
179
|
+
connect_by do |parent_table, child_table|
|
180
|
+
parent_table[:id].eq child_table[:parent_id]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
### ORDER SIBLINGS
|
186
|
+
|
187
|
+
You can specify order in which rows on each hierarchy level should appear:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
Category.join_recursive { order_siblings(:name) }
|
191
|
+
|
192
|
+
# you can reverse order
|
193
|
+
Category.join_recursive { order_siblings(name: :desc) }
|
194
|
+
|
195
|
+
# arbitrary strings and Arel nodes are allowed also
|
196
|
+
Category.join_recursive { order_siblings('name ASC') }
|
197
|
+
Category.join_recursive { |query| query.order_siblings(query.table[:name].asc) }
|
198
|
+
```
|
199
|
+
|
200
|
+
### WHERE conditions
|
201
|
+
|
202
|
+
You can filter rows on each hierarchy level by applying `WHERE` conditions:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Category.join_recursive do
|
206
|
+
connect_by(id: :parent_id).where('name LIKE ?', 'ruby %')
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
You can even refer to parent table, just don't forget to include columns in `SELECT` clause!
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
Category.join_recursive do |query|
|
214
|
+
query.connect_by(id: :parent_id)
|
215
|
+
.select(:name).
|
216
|
+
.where(query.prior[:name].matches('ruby %'))
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
Or, if Arel semantics does not fit your needs:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
Category.join_recursive do |query|
|
224
|
+
query.connect_by(id: :parent_id)
|
225
|
+
.where("#{query.prior.name}.name LIKE ?", 'ruby %')
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
### NOCYCLE
|
230
|
+
|
231
|
+
Recursive query will loop if hierarchy contains cycles (your graph is not acyclic).
|
232
|
+
`NOCYCLE` clause, which is turned off by default, could prevent it.
|
233
|
+
|
234
|
+
Loop example:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
node_1 = Category.create
|
238
|
+
node_2 = Category.create(parent: node_1)
|
239
|
+
|
240
|
+
node_1.parent = node_2
|
241
|
+
node_1.save
|
242
|
+
```
|
243
|
+
|
244
|
+
`node_1` and `node_2` now link to each other, so following query will never end:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
Category.join_recursive do |query|
|
248
|
+
query.connect_by(id: :parent_id)
|
249
|
+
.start_with(id: node_1.id)
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
`#nocycle` method will prevent endless loop:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
Category.join_recursive do |query|
|
257
|
+
query.connect_by(id: :parent_id)
|
258
|
+
.start_with(id: node_1.id)
|
259
|
+
.nocycle
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
## DISTINCT
|
264
|
+
By default, the union term in the Common Table Expression uses a `UNION ALL`. If you want
|
265
|
+
to `SELECT DISTINCT` CTE values, add a query option for `distinct`:
|
266
|
+
```ruby
|
267
|
+
Category.join_recursive do |query|
|
268
|
+
query.connect_by(id: :parent_id)
|
269
|
+
.start_with(id: node_1.id)
|
270
|
+
.distinct
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
If you want to join CTE terms by `UNION DISTINCT`, pass an option to `join_recursive`:
|
275
|
+
```ruby
|
276
|
+
Category.join_recursive(union_type: :distinct) do |query|
|
277
|
+
query.connect_by(id: :parent_id)
|
278
|
+
.start_with(id: node_1.id)
|
279
|
+
end
|
280
|
+
```
|
281
|
+
|
282
|
+
## Generated SQL queries
|
283
|
+
|
284
|
+
Under the hood this extensions builds `INNER JOIN` to recursive subquery.
|
285
|
+
|
286
|
+
For example, this piece of code
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
Category.join_recursive do |query|
|
290
|
+
query.start_with(parent_id: nil) { select('0 LEVEL') }
|
291
|
+
.connect_by(id: :parent_id)
|
292
|
+
.select(:depth)
|
293
|
+
.select(query.prior[:LEVEL] + 1, start_with: false)
|
294
|
+
.where(query.prior[:depth].lteq(5))
|
295
|
+
.order_siblings(:position)
|
296
|
+
.nocycle
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
would generate following SQL (if PostgreSQL used):
|
301
|
+
|
302
|
+
```sql
|
303
|
+
SELECT "categories".*
|
304
|
+
FROM "categories" INNER JOIN (
|
305
|
+
WITH RECURSIVE "categories__recursive" AS (
|
306
|
+
SELECT depth,
|
307
|
+
0 LEVEL,
|
308
|
+
"categories"."id",
|
309
|
+
"categories"."parent_id",
|
310
|
+
ARRAY["categories"."position"] AS __order_column,
|
311
|
+
ARRAY["categories"."id"] AS __path
|
312
|
+
FROM "categories"
|
313
|
+
WHERE "categories"."parent_id" IS NULL
|
314
|
+
|
315
|
+
UNION ALL
|
316
|
+
|
317
|
+
SELECT "categories"."depth",
|
318
|
+
"categories__recursive"."LEVEL" + 1,
|
319
|
+
"categories"."id",
|
320
|
+
"categories"."parent_id",
|
321
|
+
"categories__recursive"."__order_column" || "categories"."position",
|
322
|
+
"categories__recursive"."__path" || "categories"."id"
|
323
|
+
FROM "categories" INNER JOIN
|
324
|
+
"categories__recursive" ON "categories__recursive"."id" = "categories"."parent_id"
|
325
|
+
WHERE ("categories__recursive"."depth" <= 5) AND
|
326
|
+
NOT ("categories"."id" = ANY("categories__recursive"."__path"))
|
327
|
+
)
|
328
|
+
SELECT "categories__recursive".* FROM "categories__recursive"
|
329
|
+
) AS "categories__recursive" ON "categories"."id" = "categories__recursive"."id"
|
330
|
+
ORDER BY "categories__recursive"."__order_column" ASC
|
331
|
+
```
|
332
|
+
|
333
|
+
If you want to use a `LEFT OUTER JOIN` instead of an `INNER JOIN`, add a query option for `outer_join_hierarchical`. This option allows the query to return non-hierarchical entries:
|
334
|
+
```ruby
|
335
|
+
.join_recursive(outer_join_hierarchical: true)
|
336
|
+
```
|
337
|
+
|
338
|
+
If, when joining the recursive view to the main table, you want to change the foreign_key on the recursive view from the primary key of the main table to another column:
|
339
|
+
```ruby
|
340
|
+
.join_recursive(foreign_key: another_column)
|
341
|
+
```
|
342
|
+
|
343
|
+
## Related resources
|
344
|
+
|
345
|
+
* [About hierarchical queries (Wikipedia)](http://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL)
|
346
|
+
* [Hierarchical queries in Oracle](http://docs.oracle.com/cd/B19306_01/server.102/b14200/queries003.htm)
|
347
|
+
* [Recursive queries in PostgreSQL](http://www.postgresql.org/docs/9.3/static/queries-with.html)
|
348
|
+
* [Using Recursive SQL with ActiveRecord trees](http://hashrocket.com/blog/posts/recursive-sql-in-activerecord)
|
349
|
+
|
350
|
+
## Contributing
|
351
|
+
|
352
|
+
1. Fork it ( http://github.com/take-five/activerecord-hierarchical_query/fork )
|
353
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
354
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
355
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
356
|
+
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/query'
|
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::Query] query Hierarchical query
|
30
|
+
# @raise [ArgumentError] if block is omitted
|
31
|
+
def join_recursive(join_options = {}, &block)
|
32
|
+
raise ArgumentError, 'block expected' unless block_given?
|
33
|
+
|
34
|
+
query = Query.new(klass)
|
35
|
+
|
36
|
+
if block.arity == 0
|
37
|
+
query.instance_eval(&block)
|
38
|
+
else
|
39
|
+
block.call(query)
|
40
|
+
end
|
41
|
+
|
42
|
+
query.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,26 @@
|
|
1
|
+
require 'arel/visitors/depth_first'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module HierarchicalQuery
|
5
|
+
module CTE
|
6
|
+
class Columns
|
7
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
8
|
+
def initialize(query)
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
# returns columns to be selected from both recursive and non-recursive terms
|
13
|
+
def to_a
|
14
|
+
column_names = [@query.klass.primary_key] | connect_by_columns
|
15
|
+
column_names.map { |name| @query.table[name] }
|
16
|
+
end
|
17
|
+
alias_method :to_ary, :to_a
|
18
|
+
|
19
|
+
private
|
20
|
+
def connect_by_columns
|
21
|
+
@query.join_conditions.grep(Arel::Attributes::Attribute) { |column| column.name.to_s }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|