activerecord-hierarchical_query 0.0.2 → 0.0.5
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 +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
|