activerecord-recursive_tree_scopes 0.1.2 → 0.2.0
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 +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
|