cuatlan-activerecord-hierarchical_query 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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
+ [![Build Status](https://travis-ci.org/take-five/activerecord-hierarchical_query.png?branch=master)](https://travis-ci.org/take-five/activerecord-hierarchical_query)
10
+ [![Code Climate](https://codeclimate.com/github/take-five/activerecord-hierarchical_query.png)](https://codeclimate.com/github/take-five/activerecord-hierarchical_query)
11
+ [![Coverage Status](https://coveralls.io/repos/take-five/activerecord-hierarchical_query/badge.png)](https://coveralls.io/r/take-five/activerecord-hierarchical_query)
12
+ [![Dependency Status](https://gemnasium.com/take-five/activerecord-hierarchical_query.png)](https://gemnasium.com/take-five/activerecord-hierarchical_query)
13
+ [![Gem Version](https://badge.fury.io/rb/activerecord-hierarchical_query.png)](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