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