edge 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE +22 -0
- data/README.md +91 -0
- data/Rakefile +2 -0
- data/bench/benchmark_helper.rb +19 -0
- data/bench/database.yml +4 -0
- data/bench/database_structure.sql +9 -0
- data/bench/forest_find.rb +71 -0
- data/edge.gemspec +24 -0
- data/lib/edge.rb +8 -0
- data/lib/edge/forest.rb +146 -0
- data/lib/edge/version.rb +3 -0
- data/spec/database.yml +4 -0
- data/spec/database_structure.sql +7 -0
- data/spec/forest_spec.rb +188 -0
- data/spec/spec_helper.rb +41 -0
- metadata +120 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jack Christensen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Edge
|
2
|
+
|
3
|
+
Edge provides graph functionality to ActiveRecord using recursive common table
|
4
|
+
expressions. It has only been tested with PostgreSQL, but it uses Arel for
|
5
|
+
SQL generation so it should work with any database and adapter that support
|
6
|
+
recursive CTEs.
|
7
|
+
|
8
|
+
acts_as_forest enables an entire tree or even an entire forest of trees to
|
9
|
+
be loaded in a single query. All parent and children associations are
|
10
|
+
preloaded.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'edge'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install edge
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
acts_as_forest adds tree / multi-tree functionality. All it needs a parent_id
|
29
|
+
column. This can be overridden by passing a :foreign_key option to
|
30
|
+
acts_as_forest.
|
31
|
+
|
32
|
+
class Location < ActiveRecord::Base
|
33
|
+
acts_as_forest :order => "name"
|
34
|
+
end
|
35
|
+
|
36
|
+
usa = Location.create! :name => "USA"
|
37
|
+
illinois = usa.children.create! :name => "Illinois"
|
38
|
+
chicago = illinois.children.create! :name => "Chicago"
|
39
|
+
indiana = usa.children.create! :name => "Indiana"
|
40
|
+
canada = Location.create! :name => "Canada"
|
41
|
+
british_columbia = canada.children.create! :name => "British Columbia"
|
42
|
+
|
43
|
+
Location.root.all # [usa, canada]
|
44
|
+
Location.find_forest # [usa, canada] with all children and parents preloaded
|
45
|
+
Location.find_tree usa.id # load a single tree.
|
46
|
+
|
47
|
+
## Benchmarks
|
48
|
+
|
49
|
+
Edge includes a performance benchmarks. You can create test forests with a
|
50
|
+
configurable number of trees, depth, number of children per node, and
|
51
|
+
size of payload per node.
|
52
|
+
|
53
|
+
jack@moya:~/work/edge$ ruby -I lib -I bench bench/forest_find.rb --help
|
54
|
+
Usage: forest_find [options]
|
55
|
+
-t, --trees NUM Number of trees to create
|
56
|
+
-d, --depth NUM Depth of trees
|
57
|
+
-c, --children NUM Number of children per node
|
58
|
+
-p, --payload NUM Characters of payload per node
|
59
|
+
|
60
|
+
Even on slower machines entire trees can be loaded quickly.
|
61
|
+
|
62
|
+
jack@moya:~/work/edge$ ruby -I lib -I bench bench/forest_find.rb
|
63
|
+
Trees: 50
|
64
|
+
Depth: 3
|
65
|
+
Children per node: 10
|
66
|
+
Payload characters per node: 16
|
67
|
+
Descendants per tree: 110
|
68
|
+
Total records: 5550
|
69
|
+
user system total real
|
70
|
+
Load entire forest 10 times 4.260000 0.010000 4.270000 ( 4.422442)
|
71
|
+
Load one tree 100 times 0.830000 0.040000 0.870000 ( 0.984642)
|
72
|
+
|
73
|
+
### Running the benchmarks
|
74
|
+
|
75
|
+
1. Create a database such as edge_bench.
|
76
|
+
2. Configure bench/database.yml to connect to it.
|
77
|
+
3. Load bench/database_structure.sql into your bench database.
|
78
|
+
4. Run benchmark scripts from root of gem directory (remember to pass ruby
|
79
|
+
the include paths for lib and bench)
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
1. Fork it
|
84
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
85
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
86
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
87
|
+
5. Create new Pull Request
|
88
|
+
|
89
|
+
## License
|
90
|
+
|
91
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'edge'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
database_config = YAML.load_file(File.expand_path("../database.yml", __FILE__))
|
7
|
+
ActiveRecord::Base.establish_connection database_config["bench"]
|
8
|
+
|
9
|
+
class ActsAsForestRecord < ActiveRecord::Base
|
10
|
+
acts_as_forest
|
11
|
+
end
|
12
|
+
|
13
|
+
def clean_database
|
14
|
+
ActsAsForestRecord.delete_all
|
15
|
+
end
|
16
|
+
|
17
|
+
def vacuum_analyze
|
18
|
+
ActiveRecord::Base.connection.execute "VACUUM ANALYZE acts_as_forest_records"
|
19
|
+
end
|
data/bench/database.yml
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'benchmark_helper'
|
2
|
+
|
3
|
+
options = {}
|
4
|
+
optparse = OptionParser.new do |opts|
|
5
|
+
options[:num_trees] = 50
|
6
|
+
opts.on '-t NUM', '--trees NUM', Integer, 'Number of trees to create' do |n|
|
7
|
+
options[:num_trees] = n
|
8
|
+
end
|
9
|
+
|
10
|
+
options[:depth] = 3
|
11
|
+
opts.on '-d NUM', '--depth NUM', Integer, 'Depth of trees' do |n|
|
12
|
+
options[:depth] = n
|
13
|
+
end
|
14
|
+
|
15
|
+
options[:num_children] = 10
|
16
|
+
opts.on '-c NUM', '--children NUM', Integer, 'Number of children per node' do |n|
|
17
|
+
options[:num_children] = n
|
18
|
+
end
|
19
|
+
|
20
|
+
options[:payload_size] = 16
|
21
|
+
opts.on '-p NUM', '--payload NUM', Integer, 'Characters of payload per node' do |n|
|
22
|
+
options[:payload_size] = n
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
optparse.parse!
|
27
|
+
|
28
|
+
NUM_TREES = options[:num_trees]
|
29
|
+
DEPTH = options[:depth]
|
30
|
+
NUM_CHILDREN = options[:num_children]
|
31
|
+
PAYLOAD_SIZE = options[:payload_size]
|
32
|
+
|
33
|
+
|
34
|
+
def create_forest_tree(current_depth = 1, parent = nil)
|
35
|
+
node = ActsAsForestRecord.create! :parent => parent, :payload => "z" * PAYLOAD_SIZE
|
36
|
+
unless current_depth == DEPTH
|
37
|
+
NUM_CHILDREN.times { create_forest_tree current_depth + 1, node }
|
38
|
+
end
|
39
|
+
node
|
40
|
+
end
|
41
|
+
|
42
|
+
clean_database
|
43
|
+
ActsAsForestRecord.transaction do
|
44
|
+
NUM_TREES.times { create_forest_tree }
|
45
|
+
end
|
46
|
+
vacuum_analyze
|
47
|
+
|
48
|
+
puts "Trees: #{NUM_TREES}"
|
49
|
+
puts "Depth: #{DEPTH}"
|
50
|
+
puts "Children per node: #{NUM_CHILDREN}"
|
51
|
+
puts "Payload characters per node: #{PAYLOAD_SIZE}"
|
52
|
+
puts "Descendants per tree: #{ActsAsForestRecord.find_tree(ActsAsForestRecord.root.first.id).descendants.size}"
|
53
|
+
puts "Total records: #{ActsAsForestRecord.count}"
|
54
|
+
|
55
|
+
|
56
|
+
Benchmark.bm(40) do |x|
|
57
|
+
load_entire_forest_times = 10
|
58
|
+
x.report("Load entire forest #{load_entire_forest_times} times") do
|
59
|
+
load_entire_forest_times.times do
|
60
|
+
ActsAsForestRecord.find_forest
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
load_one_tree_times = 100
|
65
|
+
first_tree_id = ActsAsForestRecord.root.first.id
|
66
|
+
x.report("Load one tree #{load_one_tree_times} times") do
|
67
|
+
load_one_tree_times.times do
|
68
|
+
ActsAsForestRecord.find_tree first_tree_id
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/edge.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/edge/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jack Christensen"]
|
6
|
+
gem.email = ["jack@jackchristensen.com"]
|
7
|
+
gem.description = %q{Graph functionality for ActiveRecord}
|
8
|
+
gem.summary = %q{Graph functionality for ActiveRecord. Provides tree/forest modeling structure that can load entire trees in a single query.}
|
9
|
+
gem.homepage = "https://github.com/JackC/edge"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "edge"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Edge::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'activerecord', ">= 3.2.0"
|
19
|
+
|
20
|
+
gem.add_development_dependency 'pg'
|
21
|
+
gem.add_development_dependency 'rspec', "~> 2.8.0"
|
22
|
+
gem.add_development_dependency 'guard', ">= 0.10.0"
|
23
|
+
gem.add_development_dependency 'guard-rspec', ">= 0.6.0"
|
24
|
+
end
|
data/lib/edge.rb
ADDED
data/lib/edge/forest.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
module Edge
|
2
|
+
module Forest
|
3
|
+
# acts_as_forest models a tree/multi-tree structure.
|
4
|
+
module ClassMethods
|
5
|
+
# options:
|
6
|
+
#
|
7
|
+
# * foreign_key - column name to use for parent foreign_key (default: parent_id)
|
8
|
+
# * order - how to order children (default: none)
|
9
|
+
def acts_as_forest(options={})
|
10
|
+
options.assert_valid_keys :foreign_key, :order
|
11
|
+
|
12
|
+
class_attribute :forest_foreign_key
|
13
|
+
self.forest_foreign_key = options[:foreign_key] || "parent_id"
|
14
|
+
|
15
|
+
class_attribute :forest_order
|
16
|
+
self.forest_order = options[:order] || nil
|
17
|
+
|
18
|
+
common_options = {
|
19
|
+
:class_name => self,
|
20
|
+
:foreign_key => forest_foreign_key
|
21
|
+
}
|
22
|
+
|
23
|
+
belongs_to :parent, common_options
|
24
|
+
|
25
|
+
children_options = if forest_order
|
26
|
+
common_options.merge(:order => forest_order)
|
27
|
+
else
|
28
|
+
common_options
|
29
|
+
end
|
30
|
+
|
31
|
+
has_many :children, children_options
|
32
|
+
|
33
|
+
scope :root, where(forest_foreign_key => nil)
|
34
|
+
|
35
|
+
include Edge::Forest::InstanceMethods
|
36
|
+
|
37
|
+
# Finds entire forest and preloads all associations. It can be used at
|
38
|
+
# the end of an ActiveRecord finder chain.
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
# # loads all locations
|
42
|
+
# Location.find_forest
|
43
|
+
#
|
44
|
+
# # loads all nodes with matching names and all there descendants
|
45
|
+
# Category.where(:name => %w{clothing books electronics}).find_forest
|
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)
|
62
|
+
manager.order(forest_order) if forest_order
|
63
|
+
|
64
|
+
records = find_by_sql manager.to_sql
|
65
|
+
|
66
|
+
records_by_id = records.each_with_object({}) { |r, h| h[r.id] = r }
|
67
|
+
|
68
|
+
# Set all children associations to an empty array
|
69
|
+
records.each do |r|
|
70
|
+
children_association = r.association(:children)
|
71
|
+
children_association.target = []
|
72
|
+
end
|
73
|
+
|
74
|
+
top_level_records = []
|
75
|
+
|
76
|
+
records.each do |r|
|
77
|
+
parent = records_by_id[r[forest_foreign_key]]
|
78
|
+
if parent
|
79
|
+
r.association(:parent).target = parent
|
80
|
+
parent.association(:children).target.push(r)
|
81
|
+
else
|
82
|
+
top_level_records.push(r)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
top_level_records
|
87
|
+
end
|
88
|
+
|
89
|
+
# Finds an a tree or trees by id.
|
90
|
+
#
|
91
|
+
# If any requested ids are not found it raises
|
92
|
+
# ActiveRecord::RecordNotFound.
|
93
|
+
def find_tree(id_or_ids)
|
94
|
+
trees = where(:id => id_or_ids).find_forest
|
95
|
+
if id_or_ids.kind_of?(Array)
|
96
|
+
raise ActiveRecord::RecordNotFound unless trees.size == id_or_ids.size
|
97
|
+
trees
|
98
|
+
else
|
99
|
+
raise ActiveRecord::RecordNotFound if trees.empty?
|
100
|
+
trees.first
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
module InstanceMethods
|
107
|
+
# Returns the root of this node. If this node is root returns self.
|
108
|
+
def root
|
109
|
+
parent ? parent.root : self
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns true is this node is a root or false otherwise
|
113
|
+
def root?
|
114
|
+
!parent_id
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns all sibling nodes (nodes that have the same parent). If this
|
118
|
+
# node is a root node it returns an empty array.
|
119
|
+
def siblings
|
120
|
+
parent ? parent.children - [self] : []
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns all ancestors ordered by nearest ancestors first.
|
124
|
+
def ancestors
|
125
|
+
_ancestors = []
|
126
|
+
node = self
|
127
|
+
while(node = node.parent)
|
128
|
+
_ancestors.push(node)
|
129
|
+
end
|
130
|
+
|
131
|
+
_ancestors
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns all descendants
|
135
|
+
def descendants
|
136
|
+
if children.present?
|
137
|
+
children + children.map(&:descendants).flatten
|
138
|
+
else
|
139
|
+
[]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
ActiveRecord::Base.extend Edge::Forest::ClassMethods
|
data/lib/edge/version.rb
ADDED
data/spec/database.yml
ADDED
data/spec/forest_spec.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Location < ActiveRecord::Base
|
4
|
+
acts_as_forest :order => "name"
|
5
|
+
end
|
6
|
+
|
7
|
+
describe "Edge::Forest" do
|
8
|
+
let!(:usa) { Location.create! :name => "USA" }
|
9
|
+
let!(:illinois) { Location.create! :parent => usa, :name => "Illinois" }
|
10
|
+
let!(:chicago) { Location.create! :parent => illinois, :name => "Chicago" }
|
11
|
+
let!(:indiana) { Location.create! :parent => usa, :name => "Indiana" }
|
12
|
+
let!(:canada) { Location.create! :name => "Canada" }
|
13
|
+
let!(:british_columbia) { Location.create! :parent => canada, :name => "British Columbia" }
|
14
|
+
|
15
|
+
describe "root?" do
|
16
|
+
context "of root node" do
|
17
|
+
it "should be true" do
|
18
|
+
usa.root?.should == true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "of child node" do
|
23
|
+
it "should be false" do
|
24
|
+
illinois.root?.should == false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "of leaf node" do
|
29
|
+
it "should be root node" do
|
30
|
+
chicago.root?.should == false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "root" do
|
36
|
+
context "of root node" do
|
37
|
+
it "should be self" do
|
38
|
+
usa.root.should == usa
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "of child node" do
|
43
|
+
it "should be root node" do
|
44
|
+
illinois.root.should == usa
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "of leaf node" do
|
49
|
+
it "should be root node" do
|
50
|
+
chicago.root.should == usa
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "parent" do
|
56
|
+
context "of root node" do
|
57
|
+
it "should be nil" do
|
58
|
+
usa.parent.should == nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "of child node" do
|
63
|
+
it "should be parent" do
|
64
|
+
illinois.parent.should == usa
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "of leaf node" do
|
69
|
+
it "should be parent" do
|
70
|
+
chicago.parent.should == illinois
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "ancestors" do
|
76
|
+
context "of root node" do
|
77
|
+
it "should be empty" do
|
78
|
+
usa.ancestors.should be_empty
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "of leaf node" do
|
83
|
+
it "should be ancestors ordered by ascending distance" do
|
84
|
+
chicago.ancestors.should == [illinois, usa]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "siblings" do
|
90
|
+
context "of root node" do
|
91
|
+
it "should be empty" do
|
92
|
+
usa.siblings.should be_empty
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "of child node" do
|
97
|
+
it "should be other children of parent" do
|
98
|
+
illinois.siblings.should include(indiana)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "children" do
|
104
|
+
it "should be children" do
|
105
|
+
usa.children.should include(illinois, indiana)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should be ordered" do
|
109
|
+
alabama = Location.create! :parent => usa, :name => "Alabama"
|
110
|
+
usa.children.should == [alabama, illinois, indiana]
|
111
|
+
end
|
112
|
+
|
113
|
+
context "of leaf" do
|
114
|
+
it "should be empty" do
|
115
|
+
chicago.children.should be_empty
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe "descendants" do
|
121
|
+
it "should be all descendants" do
|
122
|
+
usa.descendants.should include(illinois, indiana, chicago)
|
123
|
+
end
|
124
|
+
|
125
|
+
context "of leaf" do
|
126
|
+
it "should be empty" do
|
127
|
+
chicago.descendants.should be_empty
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "root scope" do
|
133
|
+
it "returns only root nodes" do
|
134
|
+
Location.root.all.should include(usa, canada)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "find_forest" do
|
139
|
+
it "preloads all parents and children" do
|
140
|
+
forest = Location.find_forest
|
141
|
+
|
142
|
+
Location.with_scope(
|
143
|
+
:find => Location.where("purposely fail if any Location find happens here")
|
144
|
+
) do
|
145
|
+
forest.each do |tree|
|
146
|
+
tree.descendants.each do |node|
|
147
|
+
node.parent.should be
|
148
|
+
node.children.should be_kind_of(Array)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it "works when scoped" do
|
155
|
+
forest = Location.where(:name => "USA").find_forest
|
156
|
+
forest.should include(usa)
|
157
|
+
end
|
158
|
+
|
159
|
+
it "preloads children in proper order" do
|
160
|
+
alabama = Location.create! :parent => usa, :name => "Alabama"
|
161
|
+
forest = Location.find_forest
|
162
|
+
tree = forest.find { |l| l.id == usa.id }
|
163
|
+
tree.children.should == [alabama, illinois, indiana]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "find_tree" do
|
168
|
+
it "finds by id" do
|
169
|
+
tree = Location.find_tree usa.id
|
170
|
+
tree.should == usa
|
171
|
+
end
|
172
|
+
|
173
|
+
it "finds multiple trees by id" do
|
174
|
+
trees = Location.find_tree [indiana.id, illinois.id]
|
175
|
+
trees.should include(indiana, illinois)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "raises ActiveRecord::RecordNotFound when id is not found" do
|
179
|
+
expect{Location.find_tree -1}.to raise_error(ActiveRecord::RecordNotFound)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "raises ActiveRecord::RecordNotFound when not all ids are not found" do
|
183
|
+
expect{Location.find_tree [indiana.id, -1]}.to raise_error(ActiveRecord::RecordNotFound)
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'edge'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
|
6
|
+
database_config = YAML.load_file(File.expand_path("../database.yml", __FILE__))
|
7
|
+
ActiveRecord::Base.establish_connection database_config["test"]
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
# class HstoreRecord < ActiveRecord::Base
|
12
|
+
# serialize :properties, Surus::Hstore::Serializer.new
|
13
|
+
# end
|
14
|
+
|
15
|
+
# class TextArrayRecord < ActiveRecord::Base
|
16
|
+
# serialize :texts, Surus::Array::TextSerializer.new
|
17
|
+
# end
|
18
|
+
|
19
|
+
# class IntegerArrayRecord < ActiveRecord::Base
|
20
|
+
# serialize :integers, Surus::Array::IntegerSerializer.new
|
21
|
+
# end
|
22
|
+
|
23
|
+
# class FloatArrayRecord < ActiveRecord::Base
|
24
|
+
# serialize :floats, Surus::Array::FloatSerializer.new
|
25
|
+
# end
|
26
|
+
|
27
|
+
# class DecimalArrayRecord < ActiveRecord::Base
|
28
|
+
# serialize :decimals, Surus::Array::DecimalSerializer.new
|
29
|
+
# end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
RSpec.configure do |config|
|
34
|
+
config.around :disable_transactions => nil do |example|
|
35
|
+
ActiveRecord::Base.transaction do
|
36
|
+
example.call
|
37
|
+
raise ActiveRecord::Rollback
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: edge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jack Christensen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &14555680 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *14555680
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: pg
|
27
|
+
requirement: &14555260 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *14555260
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &14554720 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.8.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *14554720
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard
|
49
|
+
requirement: &14554220 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.10.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *14554220
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: guard-rspec
|
60
|
+
requirement: &14553760 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 0.6.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *14553760
|
69
|
+
description: Graph functionality for ActiveRecord
|
70
|
+
email:
|
71
|
+
- jack@jackchristensen.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- .rspec
|
78
|
+
- Gemfile
|
79
|
+
- Guardfile
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bench/benchmark_helper.rb
|
84
|
+
- bench/database.yml
|
85
|
+
- bench/database_structure.sql
|
86
|
+
- bench/forest_find.rb
|
87
|
+
- edge.gemspec
|
88
|
+
- lib/edge.rb
|
89
|
+
- lib/edge/forest.rb
|
90
|
+
- lib/edge/version.rb
|
91
|
+
- spec/database.yml
|
92
|
+
- spec/database_structure.sql
|
93
|
+
- spec/forest_spec.rb
|
94
|
+
- spec/spec_helper.rb
|
95
|
+
homepage: https://github.com/JackC/edge
|
96
|
+
licenses: []
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.8.17
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Graph functionality for ActiveRecord. Provides tree/forest modeling structure
|
119
|
+
that can load entire trees in a single query.
|
120
|
+
test_files: []
|