edge 0.1.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/.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: []
|