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 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