edge 0.1.0 → 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.
- 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
|