activerecord-recursive_tree_scopes 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -2
- data/VERSION +1 -1
- data/activerecord-recursive_tree_scopes.gemspec +4 -3
- data/lib/activerecord-recursive_tree_scopes.rb +11 -8
- data/spec/activerecord-recursive_tree_scopes_spec.rb +196 -96
- data/spec/support/create_schema.rb +10 -0
- data/spec/support/person.rb +7 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8aa1c8117447b477a47776f01fe8588ffcbe695e
|
4
|
+
data.tar.gz: 037dcf0df1b722b581642b2a3fe6ea960ffd4505
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d929d9155abc09b234d1e994887ce1c4f5e61211229b65cd4e5ec94cd7e1c78ff6ba312426872134cde98aadd1e6ace5754ca301fce4d68051ce1132ff980f0
|
7
|
+
data.tar.gz: 374c820c032e89a7c5fecd1807397676935b3eb0c332139db7b7005609ed9d6a3abdd775024990973be1d5bc39567bf6c8e84cc88c05e54ba5054a50bca5d840
|
data/README.md
CHANGED
@@ -61,6 +61,21 @@ ORDER BY employees.id
|
|
61
61
|
```
|
62
62
|
|
63
63
|
|
64
|
+
# Advanced Usage
|
65
|
+
Multiple key trees are supported. For example, a `Person` model may have
|
66
|
+
`mother_id` and `father_id` keys. That's no problem, just tell the scope about
|
67
|
+
both keys. `person.progenitors` will return all ancestors, mothers and fathers.
|
68
|
+
```ruby
|
69
|
+
class Person < ActiveRecord::Base
|
70
|
+
belongs_to :mother, class_name: 'Person'
|
71
|
+
belongs_to :father, class_name: 'Person'
|
72
|
+
|
73
|
+
has_ancestors :progenitors, key: [ :mother_id, :father_id ]
|
74
|
+
has_descendants :progeny, key: [ :mother_id, :father_id ]
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
|
64
79
|
## Friendly
|
65
80
|
|
66
81
|
Go ahead, chain away:
|
@@ -96,7 +111,7 @@ ORDER BY employees.id
|
|
96
111
|
|
97
112
|
|
98
113
|
## Requirements
|
99
|
-
* ActiveRecord >= 3.
|
114
|
+
* ActiveRecord >= 3.1.0
|
100
115
|
* PostgreSQL >= 8.4
|
101
116
|
|
102
117
|
|
@@ -105,9 +120,15 @@ ORDER BY employees.id
|
|
105
120
|
Add `gem 'activerecord-recursive_tree_scopes'` to your Gemfile.
|
106
121
|
|
107
122
|
|
123
|
+
## Alternatives
|
124
|
+
|
125
|
+
The [Edge](https://github.com/JackC/edge) gem is similar but makes better use
|
126
|
+
of Arel and relations.
|
127
|
+
|
128
|
+
|
108
129
|
## Thanks
|
109
130
|
|
110
|
-
Thanks to [Joshua Davey](https://github.com/jgdavey)
|
131
|
+
Thanks to [Joshua Davey](https://github.com/jgdavey), his
|
111
132
|
[blog post](http://hashrocket.com/blog/posts/recursive-sql-in-activerecord)
|
112
133
|
inspired this gem.
|
113
134
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "activerecord-recursive_tree_scopes"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["John Wulff"]
|
12
|
-
s.date = "2013-10-
|
12
|
+
s.date = "2013-10-02"
|
13
13
|
s.description = "Using an ActiveRecord scope, recursively query trees."
|
14
14
|
s.email = "johnw@orcasnet.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -37,7 +37,8 @@ Gem::Specification.new do |s|
|
|
37
37
|
"spec/database.yml.travis",
|
38
38
|
"spec/spec_helper.rb",
|
39
39
|
"spec/support/create_schema.rb",
|
40
|
-
"spec/support/employee.rb"
|
40
|
+
"spec/support/employee.rb",
|
41
|
+
"spec/support/person.rb"
|
41
42
|
]
|
42
43
|
s.homepage = "http://github.com/jwulff/activerecord-recursive_tree_scopes"
|
43
44
|
s.licenses = ["MIT"]
|
@@ -2,7 +2,7 @@ module RecursiveTreeScopes
|
|
2
2
|
module ModelMixin
|
3
3
|
def has_ancestors(ancestors_name = :ancestors, options = {})
|
4
4
|
options[:key] ||= :parent_id
|
5
|
-
|
5
|
+
|
6
6
|
scope ancestors_name, lambda{ |record|
|
7
7
|
RecursiveTreeScopes::Scopes.ancestors_for(record, options[:key])
|
8
8
|
}
|
@@ -36,20 +36,21 @@ module RecursiveTreeScopes
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def ancestors_sql_for(instance, key)
|
39
|
+
keys = [ key ].flatten
|
39
40
|
tree_sql = <<-SQL
|
40
|
-
WITH RECURSIVE ancestor_search(id, #{
|
41
|
-
SELECT id, #{
|
41
|
+
WITH RECURSIVE ancestor_search(id, #{keys.join ', '}, path) AS (
|
42
|
+
SELECT id, #{keys.join ', '}, ARRAY[id]
|
42
43
|
FROM #{instance.class.table_name}
|
43
44
|
WHERE id = #{instance.id}
|
44
45
|
UNION ALL
|
45
|
-
SELECT #{instance.class.table_name}.id, #{instance.class.table_name}.#{key}, path || #{instance.class.table_name}.id
|
46
|
+
SELECT #{instance.class.table_name}.id, #{keys.collect{|key| "#{instance.class.table_name}.#{key}" }.join ', '}, path || #{instance.class.table_name}.id
|
46
47
|
FROM #{instance.class.table_name}, ancestor_search
|
47
|
-
WHERE ancestor_search.#{key} = #{instance.class.table_name}.id
|
48
|
+
WHERE #{keys.collect{ |key| "ancestor_search.#{key} = #{instance.class.table_name}.id" }.join ' OR '}
|
48
49
|
)
|
49
50
|
SELECT id
|
50
51
|
FROM ancestor_search
|
51
52
|
WHERE id != #{instance.id}
|
52
|
-
ORDER BY path
|
53
|
+
ORDER BY array_length(path, 1), path
|
53
54
|
SQL
|
54
55
|
tree_sql.gsub(/\s{2,}/, ' ')
|
55
56
|
end
|
@@ -59,6 +60,7 @@ module RecursiveTreeScopes
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def descendants_sql_for(instance, key)
|
63
|
+
keys = [ key ].flatten
|
62
64
|
tree_sql = <<-SQL
|
63
65
|
WITH RECURSIVE descendants_search(id, path) AS (
|
64
66
|
SELECT id, ARRAY[id]
|
@@ -67,13 +69,14 @@ module RecursiveTreeScopes
|
|
67
69
|
UNION ALL
|
68
70
|
SELECT #{instance.class.table_name}.id, path || #{instance.class.table_name}.id
|
69
71
|
FROM descendants_search
|
70
|
-
JOIN #{instance.class.table_name}
|
72
|
+
JOIN #{instance.class.table_name}
|
73
|
+
ON #{keys.collect{ |key| "descendants_search.id = #{instance.class.table_name}.#{key}" }.join ' OR '}
|
71
74
|
WHERE NOT #{instance.class.table_name}.id = ANY(path)
|
72
75
|
)
|
73
76
|
SELECT id
|
74
77
|
FROM descendants_search
|
75
78
|
WHERE id != #{instance.id}
|
76
|
-
ORDER BY path
|
79
|
+
ORDER BY array_length(path, 1), path
|
77
80
|
SQL
|
78
81
|
tree_sql.gsub(/\s{2,}/, ' ')
|
79
82
|
end
|
@@ -1,112 +1,212 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
3
|
describe "ActiverecordRecursiveTreeScopes" do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
4
|
+
describe 'simple tree' do
|
5
|
+
let!(:alonso) { Employee.create! name: 'Alonso' }
|
6
|
+
let!(:alfred) { Employee.create! name: 'Alfred' }
|
7
|
+
let!(:barry) { Employee.create! name: 'Barry', manager: alfred }
|
8
|
+
let!(:bob) { Employee.create! name: 'Bob', manager: alfred }
|
9
|
+
let!(:charles) { Employee.create! name: 'Charles', manager: barry }
|
10
|
+
let!(:cameron) { Employee.create! name: 'Cameron', manager: barry }
|
11
|
+
let!(:carl) { Employee.create! name: 'Carl', manager: barry }
|
12
|
+
let!(:dave) { Employee.create! name: 'Dave', manager: carl }
|
13
|
+
let!(:daryl) { Employee.create! name: 'Daryl', manager: charles }
|
14
|
+
let!(:dick) { Employee.create! name: 'Dick', manager: charles }
|
15
|
+
let!(:edward) { Employee.create! name: 'Edward', manager: dick }
|
16
|
+
let!(:frank) { Employee.create! name: 'Frank', manager: edward }
|
17
|
+
|
18
|
+
describe 'chaining' do
|
19
|
+
describe 'ancestors' do
|
20
|
+
it do
|
21
|
+
frank.managers.where(name: 'Barry').should == [ barry ]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'descendants' do
|
26
|
+
it do
|
27
|
+
barry.managed.where("name LIKE ?", 'D%').should == [ dave, daryl, dick ]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'Alonso' do
|
33
|
+
subject { alonso }
|
34
|
+
its(:manager) { should be_nil }
|
35
|
+
its(:directly_managed) { should == [] }
|
36
|
+
its(:managers) { should == [] }
|
37
|
+
its(:managed) { should == [] }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'Alfred' do
|
41
|
+
subject { alfred }
|
42
|
+
its(:manager) { should be_nil }
|
43
|
+
its(:directly_managed) { should == [ barry, bob ] }
|
44
|
+
its(:managers) { should == [] }
|
45
|
+
its(:managed) { should == [ barry, bob, charles, cameron, carl, dave, daryl, dick, edward, frank ] }
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'Barry' do
|
49
|
+
subject { barry }
|
50
|
+
its(:manager) { should == alfred }
|
51
|
+
its(:directly_managed) { should == [ charles, cameron, carl ] }
|
52
|
+
its(:managers) { should == [ alfred ] }
|
53
|
+
its(:managed) { should == [ charles, cameron, carl, dave, daryl, dick, edward, frank ] }
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'Bob' do
|
57
|
+
subject { bob }
|
58
|
+
its(:manager) { should == alfred }
|
59
|
+
its(:directly_managed) { should == [] }
|
60
|
+
its(:managers) { should == [ alfred ] }
|
61
|
+
its(:managed) { should == [] }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'Charles' do
|
65
|
+
subject { charles }
|
66
|
+
its(:manager) { should == barry }
|
67
|
+
its(:directly_managed) { should == [ daryl, dick ] }
|
68
|
+
its(:managers) { should == [ alfred, barry ] }
|
69
|
+
its(:managed) { should == [ daryl, dick, edward, frank ] }
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'Cameron' do
|
73
|
+
subject { cameron }
|
74
|
+
its(:manager) { should == barry }
|
75
|
+
its(:directly_managed) { should == [] }
|
76
|
+
its(:managers) { should == [ alfred, barry ] }
|
77
|
+
its(:managed) { should == [] }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'Carl' do
|
81
|
+
subject { carl }
|
82
|
+
its(:manager) { should == barry }
|
83
|
+
its(:directly_managed) { should == [ dave ] }
|
84
|
+
its(:managers) { should == [ alfred, barry ] }
|
85
|
+
its(:managed) { should == [ dave ] }
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'Dave' do
|
89
|
+
subject { dave }
|
90
|
+
its(:manager) { should == carl }
|
91
|
+
its(:directly_managed) { should == [] }
|
92
|
+
its(:managers) { should == [ alfred, barry, carl ] }
|
93
|
+
its(:managed) { should == [] }
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'Daryl' do
|
97
|
+
subject { daryl }
|
98
|
+
its(:manager) { should == charles }
|
99
|
+
its(:directly_managed) { should == [] }
|
100
|
+
its(:managers) { should == [ alfred, barry, charles ] }
|
101
|
+
its(:managed) { should == [] }
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'Dick' do
|
105
|
+
subject { dick }
|
106
|
+
its(:manager) { should == charles }
|
107
|
+
its(:directly_managed) { should == [ edward ] }
|
108
|
+
its(:managers) { should == [ alfred, barry, charles ] }
|
109
|
+
its(:managed) { should == [ edward, frank ] }
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'Edward' do
|
113
|
+
subject { edward }
|
114
|
+
its(:manager) { should == dick }
|
115
|
+
its(:directly_managed) { should == [ frank ] }
|
116
|
+
its(:managers) { should == [ alfred, barry, charles, dick ] }
|
117
|
+
its(:managed) { should == [ frank ] }
|
118
|
+
end
|
119
|
+
|
120
|
+
describe 'Frank' do
|
121
|
+
subject { frank }
|
122
|
+
its(:manager) { should == edward }
|
123
|
+
its(:directly_managed) { should == [] }
|
124
|
+
its(:managers) { should == [ alfred, barry, charles, dick, edward ] }
|
125
|
+
its(:managed) { should == [] }
|
126
|
+
end
|
31
127
|
end
|
32
128
|
|
33
|
-
describe '
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
129
|
+
describe 'multiple key tree' do
|
130
|
+
let!(:george) { Person.create! name: 'George' }
|
131
|
+
let!(:hazel) { Person.create! name: 'Hazel' }
|
132
|
+
let!(:bill) { Person.create! name: 'Bill', father: george, mother: hazel }
|
133
|
+
let!(:william) { Person.create! name: 'William' }
|
134
|
+
let!(:sharlyn) { Person.create! name: 'Sharlyn' }
|
135
|
+
let!(:tamara) { Person.create! name: 'Tamara', father: william, mother: sharlyn }
|
136
|
+
let!(:john) { Person.create! name: 'John', father: bill, mother: tamara }
|
137
|
+
let!(:mark) { Person.create! name: 'Mark', father: bill, mother: tamara }
|
138
|
+
let!(:buster) { Person.create! name: 'Buster', father: john }
|
40
139
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
140
|
+
describe 'George' do
|
141
|
+
subject { george }
|
142
|
+
its(:mother) { should be_nil }
|
143
|
+
its(:father) { should be_nil }
|
144
|
+
its(:progenitors) { should == [] }
|
145
|
+
its(:progeny) { should == [ bill, john, mark, buster ] }
|
146
|
+
end
|
48
147
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
148
|
+
describe 'Hazel' do
|
149
|
+
subject { hazel }
|
150
|
+
its(:mother) { should be_nil }
|
151
|
+
its(:father) { should be_nil }
|
152
|
+
its(:progenitors) { should == [] }
|
153
|
+
its(:progeny) { should == [ bill, john, mark, buster ] }
|
154
|
+
end
|
56
155
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
156
|
+
describe 'William' do
|
157
|
+
subject { william }
|
158
|
+
its(:mother) { should be_nil }
|
159
|
+
its(:father) { should be_nil }
|
160
|
+
its(:progenitors) { should == [] }
|
161
|
+
its(:progeny) { should == [ tamara, john, mark, buster ] }
|
162
|
+
end
|
64
163
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
164
|
+
describe 'Sharlyn' do
|
165
|
+
subject { sharlyn }
|
166
|
+
its(:mother) { should be_nil }
|
167
|
+
its(:father) { should be_nil }
|
168
|
+
its(:progenitors) { should == [] }
|
169
|
+
its(:progeny) { should == [ tamara, john, mark, buster ] }
|
170
|
+
end
|
72
171
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
172
|
+
describe 'Bill' do
|
173
|
+
subject { bill }
|
174
|
+
its(:mother) { should == hazel }
|
175
|
+
its(:father) { should == george }
|
176
|
+
its(:progenitors) { should == [ george, hazel ] }
|
177
|
+
its(:progeny) { should == [ john, mark, buster ] }
|
178
|
+
end
|
80
179
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
180
|
+
describe 'Tamara' do
|
181
|
+
subject { tamara }
|
182
|
+
its(:mother) { should == sharlyn }
|
183
|
+
its(:father) { should == william }
|
184
|
+
its(:progenitors) { should == [ william, sharlyn ] }
|
185
|
+
its(:progeny) { should == [ john, mark, buster ] }
|
186
|
+
end
|
88
187
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
188
|
+
describe 'John' do
|
189
|
+
subject { john }
|
190
|
+
its(:mother) { should == tamara }
|
191
|
+
its(:father) { should == bill }
|
192
|
+
its(:progenitors) { should == [ george, hazel, bill, william, sharlyn, tamara ] }
|
193
|
+
its(:progeny) { should == [ buster ] }
|
194
|
+
end
|
96
195
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
196
|
+
describe 'Mark' do
|
197
|
+
subject { mark }
|
198
|
+
its(:mother) { should == tamara }
|
199
|
+
its(:father) { should == bill }
|
200
|
+
its(:progenitors) { should == [ george, hazel, bill, william, sharlyn, tamara ] }
|
201
|
+
its(:progeny) { should == [] }
|
202
|
+
end
|
104
203
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
204
|
+
describe 'Buster' do
|
205
|
+
subject { buster }
|
206
|
+
its(:mother) { should be_nil }
|
207
|
+
its(:father) { should == john }
|
208
|
+
its(:progenitors) { should == [ george, hazel, bill, william, sharlyn, tamara, john ] }
|
209
|
+
its(:progeny) { should == [] }
|
210
|
+
end
|
111
211
|
end
|
112
212
|
end
|
@@ -6,9 +6,19 @@ class CreateSchema < ActiveRecord::Migration
|
|
6
6
|
end
|
7
7
|
|
8
8
|
execute "ALTER TABLE employees ADD CONSTRAINT employees_fk_manager_id FOREIGN KEY (manager_id) REFERENCES employees (id)"
|
9
|
+
|
10
|
+
create_table :people do |t|
|
11
|
+
t.string :name
|
12
|
+
t.integer :mother_id
|
13
|
+
t.integer :father_id
|
14
|
+
end
|
15
|
+
|
16
|
+
execute "ALTER TABLE people ADD CONSTRAINT people_fk_mother_id FOREIGN KEY (mother_id) REFERENCES people (id)"
|
17
|
+
execute "ALTER TABLE people ADD CONSTRAINT people_fk_father_id FOREIGN KEY (father_id) REFERENCES people (id)"
|
9
18
|
end
|
10
19
|
|
11
20
|
def down
|
12
21
|
drop_table :employees
|
22
|
+
drop_table :people
|
13
23
|
end
|
14
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-recursive_tree_scopes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Wulff
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-10-
|
11
|
+
date: 2013-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -109,6 +109,7 @@ files:
|
|
109
109
|
- spec/spec_helper.rb
|
110
110
|
- spec/support/create_schema.rb
|
111
111
|
- spec/support/employee.rb
|
112
|
+
- spec/support/person.rb
|
112
113
|
homepage: http://github.com/jwulff/activerecord-recursive_tree_scopes
|
113
114
|
licenses:
|
114
115
|
- MIT
|