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 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
- all_nodes = Arel::Table.new(:all_nodes)
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
- end
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
@@ -1,3 +1,3 @@
1
1
  module Edge
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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: 2012-03-11 00:00:00.000000000 Z
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: &14555680 !ruby/object:Gem::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: *14555680
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: &14555260 !ruby/object:Gem::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: *14555260
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: &14554720 !ruby/object:Gem::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: *14554720
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: &14554220 !ruby/object:Gem::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: *14554220
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: &14553760 !ruby/object:Gem::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: *14553760
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.17
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