activerecord-hierarchical_query 0.0.2 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +59 -30
- data/lib/active_record/hierarchical_query.rb +11 -11
- data/lib/active_record/hierarchical_query/cte/columns.rb +2 -15
- data/lib/active_record/hierarchical_query/cte/cycle_detector.rb +54 -0
- data/lib/active_record/hierarchical_query/cte/non_recursive_term.rb +32 -11
- data/lib/active_record/hierarchical_query/cte/query_builder.rb +82 -0
- data/lib/active_record/hierarchical_query/cte/recursive_term.rb +27 -13
- data/lib/active_record/hierarchical_query/cte/union_term.rb +10 -6
- data/lib/active_record/hierarchical_query/join_builder.rb +31 -10
- data/lib/active_record/hierarchical_query/orderings.rb +113 -0
- data/lib/active_record/hierarchical_query/{builder.rb → query.rb} +66 -36
- data/lib/active_record/hierarchical_query/version.rb +1 -1
- data/lib/arel/nodes/postgresql.rb +26 -6
- data/spec/active_record/hierarchical_query_spec.rb +56 -32
- data/spec/database.yml +1 -9
- data/spec/schema.rb +2 -2
- data/spec/spec_helper.rb +2 -4
- data/spec/support/models.rb +4 -4
- metadata +28 -33
- data/lib/active_record/hierarchical_query/adapters.rb +0 -34
- data/lib/active_record/hierarchical_query/adapters/abstract.rb +0 -41
- data/lib/active_record/hierarchical_query/adapters/postgresql.rb +0 -19
- data/lib/active_record/hierarchical_query/cte/query.rb +0 -65
- data/lib/active_record/hierarchical_query/visitors/orderings.rb +0 -87
- data/lib/active_record/hierarchical_query/visitors/postgresql/cycle_detector.rb +0 -49
- data/lib/active_record/hierarchical_query/visitors/postgresql/orderings.rb +0 -70
- data/lib/active_record/hierarchical_query/visitors/visitor.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76f5e82aca51ebd96a9cc00a729b5f418e6fa385
|
4
|
+
data.tar.gz: 3ea6fa61b8b16e6ca0e18948d9280c70417a6386
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5af1b963cdc7763d04a445c1ef248c0ac53bef531b45b15a20cc7a3743e34291a8a0b92bd79786cfec44a60c64f517679f0da57520eeb630665a23d84832b8e6
|
7
|
+
data.tar.gz: f5c489b92a0422ac00dbaec5f6df28ef931e7b835c4a0b7a3f99d95a6e66e8c2616932f19b858b6772fc91f72a9d4b640b6a684c7dbdaee5daeb347202a98301
|
data/README.md
CHANGED
@@ -10,29 +10,62 @@ Create hierarchical queries using simple DSL, recursively traverse trees using s
|
|
10
10
|
|
11
11
|
If a table contains hierarchical data, then you can select rows in hierarchical order using hierarchical query builder.
|
12
12
|
|
13
|
+
### Traverse trees
|
14
|
+
|
15
|
+
Let's say you've got an ActiveRecord model `Category` that related to itself:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class Category < ActiveRecord::Base
|
19
|
+
belongs_to :parent, class_name: 'Category'
|
20
|
+
has_many :children, foreign_key: :parent_id, class_name: 'Category'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Table definition
|
24
|
+
# create_table :categories do |t|
|
25
|
+
# t.integer :parent_id
|
26
|
+
# t.string :name
|
27
|
+
# end
|
28
|
+
```
|
13
29
|
|
14
30
|
### Traverse descendants
|
15
31
|
|
16
32
|
```ruby
|
17
33
|
Category.join_recursive do |query|
|
18
|
-
query.start_with(:
|
19
|
-
.connect_by(:
|
34
|
+
query.start_with(parent_id: nil)
|
35
|
+
.connect_by(id: :parent_id)
|
20
36
|
.order_siblings(:name)
|
21
|
-
end
|
37
|
+
end # returns ActiveRecord::Relation instance
|
22
38
|
```
|
23
39
|
|
24
40
|
### Traverse ancestors
|
25
41
|
|
26
42
|
```ruby
|
27
43
|
Category.join_recursive do |query|
|
28
|
-
query.start_with(:
|
29
|
-
.connect_by(:
|
44
|
+
query.start_with(id: 42)
|
45
|
+
.connect_by(parent_id: :id)
|
30
46
|
end
|
31
47
|
```
|
32
48
|
|
49
|
+
### Show breadcrumbs using single SQL query
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
records = Category.join_recursive do |query|
|
53
|
+
query
|
54
|
+
# assume that deepest node has depth=0
|
55
|
+
.start_with(id: 42) { select('0 depth') }
|
56
|
+
# for each ancestor decrease depth by 1, do not apply
|
57
|
+
# following expression to first level of hierarchy
|
58
|
+
.select(query.prior[:depth] - 1, start_with: false)
|
59
|
+
.connect_by(parent_id: :id)
|
60
|
+
end.order('depth ASC')
|
61
|
+
|
62
|
+
# returned value is just regular ActiveRecord::Relation instance, so you can use its methods
|
63
|
+
crumbs = records.pluck(:name).join(' / ')
|
64
|
+
```
|
65
|
+
|
33
66
|
## Requirements
|
34
67
|
|
35
|
-
* ActiveRecord >= 3.1.0
|
68
|
+
* ActiveRecord >= 3.1.0 (Rails 4.2 included)
|
36
69
|
* PostgreSQL >= 8.4
|
37
70
|
|
38
71
|
## Installation
|
@@ -57,8 +90,8 @@ and `name`. You can traverse nodes recursively starting from root rows connected
|
|
57
90
|
|
58
91
|
```ruby
|
59
92
|
Category.join_recursive do
|
60
|
-
start_with(:
|
61
|
-
connect_by(:
|
93
|
+
start_with(parent_id: nil).
|
94
|
+
connect_by(id: :parent_id).
|
62
95
|
order_siblings(:name)
|
63
96
|
end
|
64
97
|
```
|
@@ -81,10 +114,10 @@ Hierarchical queries are processed as follows:
|
|
81
114
|
|
82
115
|
* First, root rows are selected -- those rows that satisfy `START WITH` condition in
|
83
116
|
order specified by `ORDER SIBLINGS` clause. In example above it's specified by
|
84
|
-
statements `query.start_with(:
|
117
|
+
statements `query.start_with(parent_id: nil)` and `query.order_siblings(:name)`.
|
85
118
|
* Second, child rows for each root rows are selected. Each child row must satisfy
|
86
119
|
condition specified by `CONNECT BY` clause with respect to one of the root rows
|
87
|
-
(`query.connect_by(:
|
120
|
+
(`query.connect_by(id: :parent_id)` in example above). Order of child rows is
|
88
121
|
also specified by `ORDER SIBLINGS` clause.
|
89
122
|
* Successive generations of child rows are selected with respect to `CONNECT BY` clause.
|
90
123
|
First the children of each row selected in step 2 selected, then the children of those
|
@@ -95,9 +128,9 @@ Hierarchical queries are processed as follows:
|
|
95
128
|
This clause is specified by `start_with` method:
|
96
129
|
|
97
130
|
```ruby
|
98
|
-
Category.join_recursive { start_with(:
|
99
|
-
Category.join_recursive { start_with { where(:
|
100
|
-
Category.join_recursive { start_with { |root_rows| root_rows.where(:
|
131
|
+
Category.join_recursive { start_with(parent_id: nil) }
|
132
|
+
Category.join_recursive { start_with { where(parent_id: nil) } }
|
133
|
+
Category.join_recursive { start_with { |root_rows| root_rows.where(parent_id: nil) } }
|
101
134
|
```
|
102
135
|
|
103
136
|
All of these statements are equivalent.
|
@@ -108,7 +141,7 @@ This clause is necessary and specified by `connect_by` method:
|
|
108
141
|
|
109
142
|
```ruby
|
110
143
|
# join parent table ID columns and child table PARENT_ID column
|
111
|
-
Category.join_recursive { connect_by(:
|
144
|
+
Category.join_recursive { connect_by(id: :parent_id) }
|
112
145
|
|
113
146
|
# you can use block to build complex JOIN conditions
|
114
147
|
Category.join_recursive do
|
@@ -126,7 +159,7 @@ You can specify order in which rows on each hierarchy level should appear:
|
|
126
159
|
Category.join_recursive { order_siblings(:name) }
|
127
160
|
|
128
161
|
# you can reverse order
|
129
|
-
Category.join_recursive { order_siblings(:
|
162
|
+
Category.join_recursive { order_siblings(name: :desc) }
|
130
163
|
|
131
164
|
# arbitrary strings and Arel nodes are allowed also
|
132
165
|
Category.join_recursive { order_siblings('name ASC') }
|
@@ -139,7 +172,7 @@ You can filter rows on each hierarchy level by applying `WHERE` conditions:
|
|
139
172
|
|
140
173
|
```ruby
|
141
174
|
Category.join_recursive do
|
142
|
-
connect_by(:
|
175
|
+
connect_by(id: :parent_id).where('name LIKE ?', 'ruby %')
|
143
176
|
end
|
144
177
|
```
|
145
178
|
|
@@ -147,7 +180,7 @@ You can even refer to parent table, just don't forget to include columns in `SEL
|
|
147
180
|
|
148
181
|
```ruby
|
149
182
|
Category.join_recursive do |query|
|
150
|
-
query.connect_by(:
|
183
|
+
query.connect_by(id: :parent_id)
|
151
184
|
.select(:name).
|
152
185
|
.where(query.prior[:name].matches('ruby %'))
|
153
186
|
end
|
@@ -157,7 +190,7 @@ Or, if Arel semantics does not fit your needs:
|
|
157
190
|
|
158
191
|
```ruby
|
159
192
|
Category.join_recursive do |query|
|
160
|
-
query.connect_by(:
|
193
|
+
query.connect_by(id: :parent_id)
|
161
194
|
.where("#{query.prior.name}.name LIKE ?", 'ruby %')
|
162
195
|
end
|
163
196
|
```
|
@@ -171,7 +204,7 @@ Loop example:
|
|
171
204
|
|
172
205
|
```ruby
|
173
206
|
node_1 = Category.create
|
174
|
-
node_2 = Category.create(:
|
207
|
+
node_2 = Category.create(parent: node_1)
|
175
208
|
|
176
209
|
node_1.parent = node_2
|
177
210
|
node_1.save
|
@@ -181,8 +214,8 @@ node_1.save
|
|
181
214
|
|
182
215
|
```ruby
|
183
216
|
Category.join_recursive do |query|
|
184
|
-
query.connect_by(:
|
185
|
-
.start_with(:
|
217
|
+
query.connect_by(id: :parent_id)
|
218
|
+
.start_with(id: node_1.id)
|
186
219
|
end
|
187
220
|
```
|
188
221
|
|
@@ -190,8 +223,8 @@ end
|
|
190
223
|
|
191
224
|
```ruby
|
192
225
|
Category.join_recursive do |query|
|
193
|
-
query.connect_by(:
|
194
|
-
.start_with(:
|
226
|
+
query.connect_by(id: :parent_id)
|
227
|
+
.start_with(id: node_1.id)
|
195
228
|
.nocycle
|
196
229
|
end
|
197
230
|
```
|
@@ -204,10 +237,10 @@ For example, this piece of code
|
|
204
237
|
|
205
238
|
```ruby
|
206
239
|
Category.join_recursive do |query|
|
207
|
-
query.start_with(:
|
208
|
-
.connect_by(:
|
240
|
+
query.start_with(parent_id: nil) { select('0 LEVEL') }
|
241
|
+
.connect_by(id: :parent_id)
|
209
242
|
.select(:depth)
|
210
|
-
.select(query.prior[:LEVEL] + 1, :
|
243
|
+
.select(query.prior[:LEVEL] + 1, start_with: false)
|
211
244
|
.where(query.prior[:depth].lteq(5))
|
212
245
|
.order_siblings(:position)
|
213
246
|
.nocycle
|
@@ -247,10 +280,6 @@ FROM "categories" INNER JOIN (
|
|
247
280
|
ORDER BY "categories__recursive"."__order_column" ASC
|
248
281
|
```
|
249
282
|
|
250
|
-
## Future plans
|
251
|
-
|
252
|
-
* Oracle support
|
253
|
-
|
254
283
|
## Related resources
|
255
284
|
|
256
285
|
* [About hierarchical queries (Wikipedia)](http://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'active_support/lazy_load_hooks'
|
4
4
|
|
5
5
|
require 'active_record/hierarchical_query/version'
|
6
|
-
require 'active_record/hierarchical_query/
|
6
|
+
require 'active_record/hierarchical_query/query'
|
7
7
|
require 'active_record/version'
|
8
8
|
|
9
9
|
module ActiveRecord
|
@@ -16,37 +16,37 @@ module ActiveRecord
|
|
16
16
|
#
|
17
17
|
# @example
|
18
18
|
# MyModel.join_recursive do |query|
|
19
|
-
# query.start_with(:
|
20
|
-
# .connect_by(:
|
19
|
+
# query.start_with(parent_id: nil)
|
20
|
+
# .connect_by(id: :parent_id)
|
21
21
|
# .where('depth < ?', 5)
|
22
|
-
# .order_siblings(:
|
22
|
+
# .order_siblings(name: :desc)
|
23
23
|
# end
|
24
24
|
#
|
25
25
|
# @param [Hash] join_options
|
26
26
|
# @option join_options [String, Symbol] :as aliased name of joined
|
27
27
|
# table (`%table_name%__recursive` by default)
|
28
28
|
# @yield [query]
|
29
|
-
# @yieldparam [ActiveRecord::HierarchicalQuery::
|
29
|
+
# @yieldparam [ActiveRecord::HierarchicalQuery::Query] query Hierarchical query
|
30
30
|
# @raise [ArgumentError] if block is omitted
|
31
31
|
def join_recursive(join_options = {}, &block)
|
32
32
|
raise ArgumentError, 'block expected' unless block_given?
|
33
33
|
|
34
|
-
|
34
|
+
query = Query.new(klass)
|
35
35
|
|
36
36
|
if block.arity == 0
|
37
|
-
|
37
|
+
query.instance_eval(&block)
|
38
38
|
else
|
39
|
-
block.call(
|
39
|
+
block.call(query)
|
40
40
|
end
|
41
41
|
|
42
|
-
|
42
|
+
query.join_to(self, join_options)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
ActiveSupport.on_load(:active_record, :
|
47
|
+
ActiveSupport.on_load(:active_record, yield: true) do |base|
|
48
48
|
class << base
|
49
|
-
delegate :join_recursive, :
|
49
|
+
delegate :join_recursive, to: ActiveRecord::HierarchicalQuery::DELEGATOR_SCOPE
|
50
50
|
end
|
51
51
|
|
52
52
|
ActiveRecord::Relation.send :include, ActiveRecord::HierarchicalQuery
|
@@ -4,7 +4,7 @@ module ActiveRecord
|
|
4
4
|
module HierarchicalQuery
|
5
5
|
module CTE
|
6
6
|
class Columns
|
7
|
-
# @param [ActiveRecord::HierarchicalQuery::
|
7
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
8
8
|
def initialize(query)
|
9
9
|
@query = query
|
10
10
|
end
|
@@ -18,20 +18,7 @@ module ActiveRecord
|
|
18
18
|
|
19
19
|
private
|
20
20
|
def connect_by_columns
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def extractor
|
25
|
-
target = []
|
26
|
-
|
27
|
-
->(arel) {
|
28
|
-
visitor = Arel::Visitors::DepthFirst.new do |node|
|
29
|
-
target << node if node.is_a?(Arel::Attributes::Attribute)
|
30
|
-
end
|
31
|
-
visitor.accept(arel)
|
32
|
-
|
33
|
-
target
|
34
|
-
}
|
21
|
+
@query.join_conditions.grep(Arel::Attributes::Attribute) { |column| column.name.to_s }
|
35
22
|
end
|
36
23
|
end
|
37
24
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module HierarchicalQuery
|
3
|
+
module CTE
|
4
|
+
class CycleDetector
|
5
|
+
COLUMN_NAME = '__path'.freeze
|
6
|
+
|
7
|
+
delegate :klass, :table, to: :@query
|
8
|
+
|
9
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
10
|
+
def initialize(query)
|
11
|
+
@query = query
|
12
|
+
end
|
13
|
+
|
14
|
+
def apply_to_non_recursive(arel)
|
15
|
+
if enabled?
|
16
|
+
arel.project Arel::Nodes::PostgresArray.new([primary_key]).as(column_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
arel
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_to_recursive(arel)
|
23
|
+
if enabled?
|
24
|
+
arel.project Arel::Nodes::ArrayConcat.new(parent_column, primary_key)
|
25
|
+
arel.constraints << Arel::Nodes::Not.new(primary_key.eq(any(parent_column)))
|
26
|
+
end
|
27
|
+
|
28
|
+
arel
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def enabled?
|
33
|
+
@query.nocycle_value
|
34
|
+
end
|
35
|
+
|
36
|
+
def column_name
|
37
|
+
COLUMN_NAME
|
38
|
+
end
|
39
|
+
|
40
|
+
def parent_column
|
41
|
+
@query.recursive_table[column_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def primary_key
|
45
|
+
table[klass.primary_key]
|
46
|
+
end
|
47
|
+
|
48
|
+
def any(argument)
|
49
|
+
Arel::Nodes::NamedFunction.new('ANY', [argument])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -6,26 +6,47 @@ module ActiveRecord
|
|
6
6
|
class NonRecursiveTerm
|
7
7
|
DISALLOWED_CLAUSES = :order, :limit, :offset, :group, :having
|
8
8
|
|
9
|
-
attr_reader :
|
10
|
-
delegate :
|
11
|
-
delegate :start_with_value, :klass, :to => :builder
|
9
|
+
attr_reader :builder
|
10
|
+
delegate :query, to: :builder
|
12
11
|
|
13
|
-
# @param [ActiveRecord::HierarchicalQuery::CTE::
|
14
|
-
def initialize(
|
15
|
-
@
|
12
|
+
# @param [ActiveRecord::HierarchicalQuery::CTE::QueryBuilder] builder
|
13
|
+
def initialize(builder)
|
14
|
+
@builder = builder
|
15
|
+
end
|
16
|
+
|
17
|
+
def bind_values
|
18
|
+
scope.bind_values
|
16
19
|
end
|
17
20
|
|
18
21
|
def arel
|
19
|
-
arel = scope.
|
20
|
-
.except(*DISALLOWED_CLAUSES)
|
21
|
-
.arel
|
22
|
+
arel = scope.arel
|
22
23
|
|
23
|
-
|
24
|
+
builder.cycle_detector.apply_to_non_recursive(arel)
|
24
25
|
end
|
25
26
|
|
26
27
|
private
|
27
28
|
def scope
|
28
|
-
|
29
|
+
@scope ||= query.
|
30
|
+
start_with_value.
|
31
|
+
select(columns).
|
32
|
+
except(*DISALLOWED_CLAUSES)
|
33
|
+
end
|
34
|
+
|
35
|
+
def columns
|
36
|
+
columns = builder.columns.to_a
|
37
|
+
|
38
|
+
if query.orderings.any?
|
39
|
+
columns << ordering
|
40
|
+
end
|
41
|
+
|
42
|
+
columns
|
43
|
+
end
|
44
|
+
|
45
|
+
def ordering
|
46
|
+
value = query.orderings.row_number_expression
|
47
|
+
column_name = query.ordering_column_name
|
48
|
+
|
49
|
+
Arel::Nodes::PostgresArray.new([value]).as(column_name)
|
29
50
|
end
|
30
51
|
end # class NonRecursiveTerm
|
31
52
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'active_record/hierarchical_query/cte/columns'
|
4
|
+
require 'active_record/hierarchical_query/cte/cycle_detector'
|
5
|
+
require 'active_record/hierarchical_query/cte/union_term'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module HierarchicalQuery
|
9
|
+
module CTE
|
10
|
+
# CTE query builder
|
11
|
+
class QueryBuilder
|
12
|
+
attr_reader :query,
|
13
|
+
:columns,
|
14
|
+
:cycle_detector
|
15
|
+
|
16
|
+
delegate :klass, :table, :recursive_table, to: :query
|
17
|
+
|
18
|
+
# @param [ActiveRecord::HierarchicalQuery::Query] query
|
19
|
+
def initialize(query)
|
20
|
+
@query = query
|
21
|
+
@columns = Columns.new(@query)
|
22
|
+
@cycle_detector = CycleDetector.new(@query)
|
23
|
+
end
|
24
|
+
|
25
|
+
def bind_values
|
26
|
+
union_term.bind_values
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Arel::SelectManager]
|
30
|
+
def build_arel
|
31
|
+
build_manager
|
32
|
+
build_select
|
33
|
+
build_limits
|
34
|
+
build_order
|
35
|
+
|
36
|
+
@arel
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def build_manager
|
41
|
+
@arel = Arel::SelectManager.new(table.engine).
|
42
|
+
with(:recursive, with_query).
|
43
|
+
from(recursive_table)
|
44
|
+
end
|
45
|
+
|
46
|
+
# "categories__recursive" AS (
|
47
|
+
# SELECT ... FROM "categories"
|
48
|
+
# UNION ALL
|
49
|
+
# SELECT ... FROM "categories"
|
50
|
+
# INNER JOIN "categories__recursive" ON ...
|
51
|
+
# )
|
52
|
+
def with_query
|
53
|
+
Arel::Nodes::As.new(recursive_table, union_term.arel)
|
54
|
+
end
|
55
|
+
|
56
|
+
def union_term
|
57
|
+
@union_term ||= UnionTerm.new(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_select
|
61
|
+
@arel.project(recursive_table[Arel.star])
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_limits
|
65
|
+
@arel.take(query.limit_value).skip(query.offset_value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_order
|
69
|
+
@arel.order(order_column.asc) if should_order?
|
70
|
+
end
|
71
|
+
|
72
|
+
def should_order?
|
73
|
+
query.orderings.any? && (query.limit_value || query.offset_value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def order_column
|
77
|
+
recursive_table[query.ordering_column_name]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|