edge 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +10 -4
- data/Rakefile +12 -0
- data/lib/edge/forest.rb +51 -38
- data/lib/edge/version.rb +1 -1
- data/spec/forest_spec.rb +56 -29
- metadata +43 -14
data/README.md
CHANGED
@@ -32,18 +32,24 @@ acts_as_forest.
|
|
32
32
|
class Location < ActiveRecord::Base
|
33
33
|
acts_as_forest :order => "name"
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
usa = Location.create! :name => "USA"
|
37
37
|
illinois = usa.children.create! :name => "Illinois"
|
38
38
|
chicago = illinois.children.create! :name => "Chicago"
|
39
39
|
indiana = usa.children.create! :name => "Indiana"
|
40
40
|
canada = Location.create! :name => "Canada"
|
41
41
|
british_columbia = canada.children.create! :name => "British Columbia"
|
42
|
-
|
42
|
+
|
43
43
|
Location.root.all # [usa, canada]
|
44
44
|
Location.find_forest # [usa, canada] with all children and parents preloaded
|
45
45
|
Location.find_tree usa.id # load a single tree.
|
46
46
|
|
47
|
+
It also provides the with_descendents scope to get all currently selected
|
48
|
+
nodes and all there descendents. It can be chained after where scopes, but
|
49
|
+
must not be used after any other type of scope.
|
50
|
+
|
51
|
+
Location.where(name: "Illinois").with_descendents.all # [illinois, chicago]
|
52
|
+
|
47
53
|
## Benchmarks
|
48
54
|
|
49
55
|
Edge includes a performance benchmarks. You can create test forests with a
|
@@ -58,7 +64,7 @@ size of payload per node.
|
|
58
64
|
-p, --payload NUM Characters of payload per node
|
59
65
|
|
60
66
|
Even on slower machines entire trees can be loaded quickly.
|
61
|
-
|
67
|
+
|
62
68
|
jack@moya:~/work/edge$ ruby -I lib -I bench bench/forest_find.rb
|
63
69
|
Trees: 50
|
64
70
|
Depth: 3
|
@@ -71,7 +77,7 @@ Even on slower machines entire trees can be loaded quickly.
|
|
71
77
|
Load one tree 100 times 0.830000 0.040000 0.870000 ( 0.984642)
|
72
78
|
|
73
79
|
### Running the benchmarks
|
74
|
-
|
80
|
+
|
75
81
|
1. Create a database such as edge_bench.
|
76
82
|
2. Configure bench/database.yml to connect to it.
|
77
83
|
3. Load bench/database_structure.sql into your bench database.
|
data/Rakefile
CHANGED
@@ -1,2 +1,14 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
2
|
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
namespace :db do
|
9
|
+
desc 'bootstrap database'
|
10
|
+
task :setup do
|
11
|
+
sh "createdb edge_test"
|
12
|
+
sh "psql edge_test < spec/database_structure.sql"
|
13
|
+
end
|
14
|
+
end
|
data/lib/edge/forest.rb
CHANGED
@@ -8,32 +8,32 @@ module Edge
|
|
8
8
|
# * order - how to order children (default: none)
|
9
9
|
def acts_as_forest(options={})
|
10
10
|
options.assert_valid_keys :foreign_key, :order
|
11
|
-
|
11
|
+
|
12
12
|
class_attribute :forest_foreign_key
|
13
13
|
self.forest_foreign_key = options[:foreign_key] || "parent_id"
|
14
|
-
|
14
|
+
|
15
15
|
class_attribute :forest_order
|
16
16
|
self.forest_order = options[:order] || nil
|
17
|
-
|
17
|
+
|
18
18
|
common_options = {
|
19
19
|
:class_name => self,
|
20
20
|
:foreign_key => forest_foreign_key
|
21
21
|
}
|
22
|
-
|
22
|
+
|
23
23
|
belongs_to :parent, common_options
|
24
|
-
|
24
|
+
|
25
25
|
children_options = if forest_order
|
26
26
|
common_options.merge(:order => forest_order)
|
27
27
|
else
|
28
28
|
common_options
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
has_many :children, children_options
|
32
|
-
|
32
|
+
|
33
33
|
scope :root, where(forest_foreign_key => nil)
|
34
|
-
|
34
|
+
|
35
35
|
include Edge::Forest::InstanceMethods
|
36
|
-
|
36
|
+
|
37
37
|
# Finds entire forest and preloads all associations. It can be used at
|
38
38
|
# the end of an ActiveRecord finder chain.
|
39
39
|
#
|
@@ -44,23 +44,9 @@ module Edge
|
|
44
44
|
# # loads all nodes with matching names and all there descendants
|
45
45
|
# Category.where(:name => %w{clothing books electronics}).find_forest
|
46
46
|
def find_forest
|
47
|
-
|
48
|
-
|
49
|
-
original_term = (current_scope || scoped).arel
|
50
|
-
iterated_term = Arel::SelectManager.new Arel::Table.engine
|
51
|
-
iterated_term.from(arel_table)
|
52
|
-
.project(arel_table.columns)
|
53
|
-
.join(all_nodes)
|
54
|
-
.on(arel_table[forest_foreign_key].eq all_nodes[:id])
|
55
|
-
|
56
|
-
union = original_term.union(iterated_term)
|
57
|
-
|
58
|
-
as_statement = Arel::Nodes::As.new all_nodes, union
|
59
|
-
|
60
|
-
manager = Arel::SelectManager.new Arel::Table.engine
|
61
|
-
manager.with(:recursive, as_statement).from(all_nodes).project(Arel.star)
|
47
|
+
manager = recursive_manager.project(Arel.star)
|
62
48
|
manager.order(forest_order) if forest_order
|
63
|
-
|
49
|
+
|
64
50
|
records = find_by_sql manager.to_sql
|
65
51
|
|
66
52
|
records_by_id = records.each_with_object({}) { |r, h| h[r.id] = r }
|
@@ -72,7 +58,7 @@ module Edge
|
|
72
58
|
end
|
73
59
|
|
74
60
|
top_level_records = []
|
75
|
-
|
61
|
+
|
76
62
|
records.each do |r|
|
77
63
|
parent = records_by_id[r[forest_foreign_key]]
|
78
64
|
if parent
|
@@ -82,10 +68,10 @@ module Edge
|
|
82
68
|
top_level_records.push(r)
|
83
69
|
end
|
84
70
|
end
|
85
|
-
|
71
|
+
|
86
72
|
top_level_records
|
87
73
|
end
|
88
|
-
|
74
|
+
|
89
75
|
# Finds an a tree or trees by id.
|
90
76
|
#
|
91
77
|
# If any requested ids are not found it raises
|
@@ -100,44 +86,71 @@ module Edge
|
|
100
86
|
trees.first
|
101
87
|
end
|
102
88
|
end
|
103
|
-
|
89
|
+
|
90
|
+
# Returns a new scope that includes previously scoped records and their descendants by subsuming the previous scope into a subquery
|
91
|
+
#
|
92
|
+
# Only where scopes can precede this in a scope chain
|
93
|
+
def with_descendants
|
94
|
+
manager = recursive_manager.project('id')
|
95
|
+
unscoped.where(id: manager)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def recursive_manager
|
100
|
+
all_nodes = Arel::Table.new(:all_nodes)
|
101
|
+
|
102
|
+
original_term = (current_scope || scoped).arel
|
103
|
+
iterated_term = Arel::SelectManager.new Arel::Table.engine
|
104
|
+
iterated_term.from(arel_table)
|
105
|
+
.project(arel_table.columns)
|
106
|
+
.join(all_nodes)
|
107
|
+
.on(arel_table[forest_foreign_key].eq all_nodes[:id])
|
108
|
+
|
109
|
+
union = original_term.union(iterated_term)
|
110
|
+
|
111
|
+
as_statement = Arel::Nodes::As.new all_nodes, union
|
112
|
+
|
113
|
+
manager = Arel::SelectManager.new Arel::Table.engine
|
114
|
+
manager.with(:recursive, as_statement).from(all_nodes)
|
115
|
+
end
|
116
|
+
end
|
104
117
|
end
|
105
|
-
|
118
|
+
|
106
119
|
module InstanceMethods
|
107
120
|
# Returns the root of this node. If this node is root returns self.
|
108
121
|
def root
|
109
122
|
parent ? parent.root : self
|
110
123
|
end
|
111
|
-
|
124
|
+
|
112
125
|
# Returns true is this node is a root or false otherwise
|
113
126
|
def root?
|
114
127
|
!parent_id
|
115
128
|
end
|
116
|
-
|
129
|
+
|
117
130
|
# Returns all sibling nodes (nodes that have the same parent). If this
|
118
|
-
# node is a root node it returns an empty array.
|
131
|
+
# node is a root node it returns an empty array.
|
119
132
|
def siblings
|
120
133
|
parent ? parent.children - [self] : []
|
121
134
|
end
|
122
|
-
|
123
|
-
# Returns all ancestors ordered by nearest ancestors first.
|
135
|
+
|
136
|
+
# Returns all ancestors ordered by nearest ancestors first.
|
124
137
|
def ancestors
|
125
138
|
_ancestors = []
|
126
139
|
node = self
|
127
140
|
while(node = node.parent)
|
128
141
|
_ancestors.push(node)
|
129
142
|
end
|
130
|
-
|
143
|
+
|
131
144
|
_ancestors
|
132
145
|
end
|
133
|
-
|
146
|
+
|
134
147
|
# Returns all descendants
|
135
148
|
def descendants
|
136
149
|
if children.present?
|
137
150
|
children + children.map(&:descendants).flatten
|
138
151
|
else
|
139
152
|
[]
|
140
|
-
end
|
153
|
+
end
|
141
154
|
end
|
142
155
|
end
|
143
156
|
end
|
data/lib/edge/version.rb
CHANGED
data/spec/forest_spec.rb
CHANGED
@@ -8,28 +8,28 @@ describe "Edge::Forest" do
|
|
8
8
|
let!(:usa) { Location.create! :name => "USA" }
|
9
9
|
let!(:illinois) { Location.create! :parent => usa, :name => "Illinois" }
|
10
10
|
let!(:chicago) { Location.create! :parent => illinois, :name => "Chicago" }
|
11
|
-
let!(:indiana) { Location.create! :parent => usa, :name => "Indiana" }
|
11
|
+
let!(:indiana) { Location.create! :parent => usa, :name => "Indiana" }
|
12
12
|
let!(:canada) { Location.create! :name => "Canada" }
|
13
13
|
let!(:british_columbia) { Location.create! :parent => canada, :name => "British Columbia" }
|
14
|
-
|
14
|
+
|
15
15
|
describe "root?" do
|
16
16
|
context "of root node" do
|
17
17
|
it "should be true" do
|
18
18
|
usa.root?.should == true
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
context "of child node" do
|
23
23
|
it "should be false" do
|
24
24
|
illinois.root?.should == false
|
25
25
|
end
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
context "of leaf node" do
|
29
29
|
it "should be root node" do
|
30
30
|
chicago.root?.should == false
|
31
31
|
end
|
32
|
-
end
|
32
|
+
end
|
33
33
|
end
|
34
34
|
|
35
35
|
describe "root" do
|
@@ -38,103 +38,103 @@ describe "Edge::Forest" do
|
|
38
38
|
usa.root.should == usa
|
39
39
|
end
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
context "of child node" do
|
43
43
|
it "should be root node" do
|
44
44
|
illinois.root.should == usa
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
context "of leaf node" do
|
49
49
|
it "should be root node" do
|
50
50
|
chicago.root.should == usa
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
describe "parent" do
|
56
56
|
context "of root node" do
|
57
57
|
it "should be nil" do
|
58
58
|
usa.parent.should == nil
|
59
59
|
end
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
context "of child node" do
|
63
63
|
it "should be parent" do
|
64
64
|
illinois.parent.should == usa
|
65
65
|
end
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
context "of leaf node" do
|
69
69
|
it "should be parent" do
|
70
70
|
chicago.parent.should == illinois
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
describe "ancestors" do
|
76
76
|
context "of root node" do
|
77
77
|
it "should be empty" do
|
78
78
|
usa.ancestors.should be_empty
|
79
79
|
end
|
80
80
|
end
|
81
|
-
|
81
|
+
|
82
82
|
context "of leaf node" do
|
83
83
|
it "should be ancestors ordered by ascending distance" do
|
84
84
|
chicago.ancestors.should == [illinois, usa]
|
85
85
|
end
|
86
86
|
end
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
describe "siblings" do
|
90
90
|
context "of root node" do
|
91
91
|
it "should be empty" do
|
92
92
|
usa.siblings.should be_empty
|
93
93
|
end
|
94
94
|
end
|
95
|
-
|
95
|
+
|
96
96
|
context "of child node" do
|
97
97
|
it "should be other children of parent" do
|
98
98
|
illinois.siblings.should include(indiana)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
end
|
102
|
-
|
102
|
+
|
103
103
|
describe "children" do
|
104
104
|
it "should be children" do
|
105
105
|
usa.children.should include(illinois, indiana)
|
106
106
|
end
|
107
|
-
|
107
|
+
|
108
108
|
it "should be ordered" do
|
109
109
|
alabama = Location.create! :parent => usa, :name => "Alabama"
|
110
110
|
usa.children.should == [alabama, illinois, indiana]
|
111
111
|
end
|
112
|
-
|
112
|
+
|
113
113
|
context "of leaf" do
|
114
114
|
it "should be empty" do
|
115
115
|
chicago.children.should be_empty
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
119
|
-
|
119
|
+
|
120
120
|
describe "descendants" do
|
121
121
|
it "should be all descendants" do
|
122
122
|
usa.descendants.should include(illinois, indiana, chicago)
|
123
123
|
end
|
124
|
-
|
124
|
+
|
125
125
|
context "of leaf" do
|
126
126
|
it "should be empty" do
|
127
127
|
chicago.descendants.should be_empty
|
128
128
|
end
|
129
129
|
end
|
130
130
|
end
|
131
|
-
|
131
|
+
|
132
132
|
describe "root scope" do
|
133
133
|
it "returns only root nodes" do
|
134
134
|
Location.root.all.should include(usa, canada)
|
135
135
|
end
|
136
136
|
end
|
137
|
-
|
137
|
+
|
138
138
|
describe "find_forest" do
|
139
139
|
it "preloads all parents and children" do
|
140
140
|
forest = Location.find_forest
|
@@ -150,39 +150,66 @@ describe "Edge::Forest" do
|
|
150
150
|
end
|
151
151
|
end
|
152
152
|
end
|
153
|
-
|
153
|
+
|
154
154
|
it "works when scoped" do
|
155
155
|
forest = Location.where(:name => "USA").find_forest
|
156
156
|
forest.should include(usa)
|
157
157
|
end
|
158
|
-
|
158
|
+
|
159
159
|
it "preloads children in proper order" do
|
160
160
|
alabama = Location.create! :parent => usa, :name => "Alabama"
|
161
161
|
forest = Location.find_forest
|
162
162
|
tree = forest.find { |l| l.id == usa.id }
|
163
163
|
tree.children.should == [alabama, illinois, indiana]
|
164
164
|
end
|
165
|
+
|
166
|
+
context "with an infinite loop" do
|
167
|
+
before do
|
168
|
+
usa.update_attribute(:parent, chicago)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "does not re-loop" do
|
172
|
+
Location.find_forest
|
173
|
+
end
|
174
|
+
end
|
165
175
|
end
|
166
|
-
|
176
|
+
|
167
177
|
describe "find_tree" do
|
168
178
|
it "finds by id" do
|
169
179
|
tree = Location.find_tree usa.id
|
170
180
|
tree.should == usa
|
171
181
|
end
|
172
|
-
|
182
|
+
|
173
183
|
it "finds multiple trees by id" do
|
174
184
|
trees = Location.find_tree [indiana.id, illinois.id]
|
175
185
|
trees.should include(indiana, illinois)
|
176
186
|
end
|
177
|
-
|
187
|
+
|
178
188
|
it "raises ActiveRecord::RecordNotFound when id is not found" do
|
179
189
|
expect{Location.find_tree -1}.to raise_error(ActiveRecord::RecordNotFound)
|
180
190
|
end
|
181
|
-
|
191
|
+
|
182
192
|
it "raises ActiveRecord::RecordNotFound when not all ids are not found" do
|
183
193
|
expect{Location.find_tree [indiana.id, -1]}.to raise_error(ActiveRecord::RecordNotFound)
|
184
194
|
end
|
185
|
-
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
describe "with_descendants" do
|
199
|
+
context "unscoped" do
|
200
|
+
it "returns all records" do
|
201
|
+
Location.with_descendants.all.should =~ Location.all
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "scoped" do
|
206
|
+
it "returns a new scope that includes previously scoped records and their descendants" do
|
207
|
+
Location.where(id: canada.id).with_descendants.all.should =~ [canada, british_columbia]
|
208
|
+
end
|
209
|
+
|
210
|
+
it "is not commutative" do
|
211
|
+
Location.with_descendants.where(id: canada.id).all.should == [canada]
|
212
|
+
end
|
213
|
+
end
|
186
214
|
end
|
187
|
-
|
188
215
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: edge
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: 3.2.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.0
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: pg
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: '0'
|
33
38
|
type: :development
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: rspec
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ~>
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: 2.8.0
|
44
54
|
type: :development
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.8.0
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: guard
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ! '>='
|
@@ -54,10 +69,15 @@ dependencies:
|
|
54
69
|
version: 0.10.0
|
55
70
|
type: :development
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.10.0
|
58
78
|
- !ruby/object:Gem::Dependency
|
59
79
|
name: guard-rspec
|
60
|
-
requirement:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
61
81
|
none: false
|
62
82
|
requirements:
|
63
83
|
- - ! '>='
|
@@ -65,7 +85,12 @@ dependencies:
|
|
65
85
|
version: 0.6.0
|
66
86
|
type: :development
|
67
87
|
prerelease: false
|
68
|
-
version_requirements:
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.6.0
|
69
94
|
description: Graph functionality for ActiveRecord
|
70
95
|
email:
|
71
96
|
- jack@jackchristensen.com
|
@@ -112,9 +137,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
137
|
version: '0'
|
113
138
|
requirements: []
|
114
139
|
rubyforge_project:
|
115
|
-
rubygems_version: 1.8.
|
140
|
+
rubygems_version: 1.8.23
|
116
141
|
signing_key:
|
117
142
|
specification_version: 3
|
118
143
|
summary: Graph functionality for ActiveRecord. Provides tree/forest modeling structure
|
119
144
|
that can load entire trees in a single query.
|
120
|
-
test_files:
|
145
|
+
test_files:
|
146
|
+
- spec/database.yml
|
147
|
+
- spec/database_structure.sql
|
148
|
+
- spec/forest_spec.rb
|
149
|
+
- spec/spec_helper.rb
|