acts_as_oqgraph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +137 -0
- data/Rakefile +52 -0
- data/VERSION +1 -0
- data/lib/acts_as_oqgraph.rb +205 -0
- data/lib/graph_edge.rb +120 -0
- data/test/helper.rb +13 -0
- data/test/models/custom_test_model.rb +3 -0
- data/test/models/test_model.rb +5 -0
- data/test/test_acts_as_oqgraph.rb +195 -0
- metadata +77 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Stuart Coyle
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
ActsAsOQGraph
|
2
|
+
===========
|
3
|
+
This gem can be used with ActiveRecord to access the features of the OQGraph
|
4
|
+
MySQL plugin.
|
5
|
+
|
6
|
+
Please take a look at the Open Query Graph engine page at http://openquery.com/products/graph-engine
|
7
|
+
|
8
|
+
This provides fast graph operations on the database. It does a similar thing to the
|
9
|
+
acts_as_graph gem, which does all the graph work on the application server. There are
|
10
|
+
pros and cons of both approaches, It really depends on your needs. Both libraries
|
11
|
+
use the C++ Boost graph library at the bottom layers. OQGraph has expensive
|
12
|
+
insert operations but is very fast at delivering the graph data and finding paths in a single SQL query.
|
13
|
+
|
14
|
+
Concepts:
|
15
|
+
The term graph we are using here is a mathematical one not a pretty picture (sorry designers).
|
16
|
+
For more see: http://en.wikipedia.org/wiki/Graph_(mathematics)
|
17
|
+
A graph consists of nodes (aka vertices) connected by edges (aka edges).
|
18
|
+
In a directed graph an edge has a direction. That is, it has an 'from' node and and a 'to' node.
|
19
|
+
When tracing a path on the graph you can only go in the direction of the edge.
|
20
|
+
The OQGraph gem operates on directed graphs. To create a non directed graph you have to create two edges,
|
21
|
+
one each way between each node.
|
22
|
+
|
23
|
+
Edges can be assigned positive floating point values called weights. Weights default to 1.0
|
24
|
+
The weights are used in shortest path calculations, a path is shorter if the sum of weights over each edge is smaller.
|
25
|
+
|
26
|
+
What you can do with OQGraph:
|
27
|
+
Imagine your shiny new social networking app, FarceBook.
|
28
|
+
You have lots and lots of users each with several friends. How do you find the friends of friends?
|
29
|
+
Or even the friends of friends of friends...right up to the six degrees of separation perhaps?
|
30
|
+
|
31
|
+
Well you can do it, with some really slow and nasty SQL queries. Relational databases are good at set
|
32
|
+
based queries but no good at graph or tree based queries. The OQGraph engine is good at graph based queries,
|
33
|
+
it enables you in one simple SQL query to find all friends of friends.
|
34
|
+
Do this: user.reachable
|
35
|
+
If you really want to you can rename the reachable method so you can do this in your User model:
|
36
|
+
alias friends, reachable
|
37
|
+
Then I can call:
|
38
|
+
user.friends
|
39
|
+
and I get the whole tree of friends of friends etc...
|
40
|
+
|
41
|
+
TODO: Perhaps I should hook into ActiveRecord associations somehow so these can be named associations instead of methods.
|
42
|
+
Not really sure of how to go about this as yet
|
43
|
+
.
|
44
|
+
Imagine you have a maze to solve. With OQGraph the solution is as simple as: start_cell.shortest_path_to(finish_cell).
|
45
|
+
|
46
|
+
It's good for representing tree structures, networks, routes between cities, roads etc.
|
47
|
+
|
48
|
+
Usage:
|
49
|
+
|
50
|
+
Class Foo < ActiveRecord::Base
|
51
|
+
acts_as_oqgraph +options+
|
52
|
+
end
|
53
|
+
|
54
|
+
Options:
|
55
|
+
:class_name - The name of the edge class, defaults to current class name appended with Edge. eg FooEdge
|
56
|
+
:table_name - The name of the edge table, defaults to table name of the specified class, eg foo_edges
|
57
|
+
:oqgraph_table_name - the name of the volatile oqgraph table. Default foo_edge_oqgraph
|
58
|
+
:from_key - The from key field in the edge table. Default 'from_id'
|
59
|
+
:to_key - The to key field in the edge table. Default: 'to_id'
|
60
|
+
:weight_column - The weight field in the edge table.
|
61
|
+
|
62
|
+
Setup:
|
63
|
+
This gem requires the use of MySQL or MariaDB with the OQGraph engine plugin.
|
64
|
+
For details of this see: http://openquery.com/products/graph-engine
|
65
|
+
|
66
|
+
You will need a table for the edges with the following schema:
|
67
|
+
create_table foo_edges do |t|
|
68
|
+
t.integer from_id
|
69
|
+
t.integer to_id
|
70
|
+
t.double weight
|
71
|
+
end
|
72
|
+
The field names and table name can be changed via the options listed above.
|
73
|
+
You should be able also to extend the edge model as you wish.
|
74
|
+
|
75
|
+
The gem will automatically create the oqgraph table and the associations to it from your node model.
|
76
|
+
The associations are:
|
77
|
+
node_model.outgoing_edges
|
78
|
+
node_model.incoming_edges
|
79
|
+
node_model.outgoing_nodes
|
80
|
+
node_model.incoming_nodes
|
81
|
+
edge_model.to
|
82
|
+
edge_model.from
|
83
|
+
|
84
|
+
Examples of use:
|
85
|
+
|
86
|
+
Creating edges:
|
87
|
+
foo.create_edge_to(bar)
|
88
|
+
foo.create_edge_to_and_from(bar)
|
89
|
+
|
90
|
+
Alternate notation using ActiveRecord associations:
|
91
|
+
foo.outgoing_nodes << bar
|
92
|
+
or equivalently:
|
93
|
+
bar.incoming_nodes << foo
|
94
|
+
At the moment you cannot add weights to edges with this style of notation.
|
95
|
+
|
96
|
+
Create a edge with weight:
|
97
|
+
bar.create_edge_to(baz, 2.0)
|
98
|
+
|
99
|
+
Removing a edge:
|
100
|
+
foo.remove_edge_to(bar)
|
101
|
+
foo.remove_edge_from(bar)
|
102
|
+
Note that these calls remove ALL edges to bar from foo
|
103
|
+
|
104
|
+
Examining edges:
|
105
|
+
|
106
|
+
What nodes point to this one?
|
107
|
+
Gives us an array of nodes that are connected to this one in the inward (from) direction.
|
108
|
+
Includes the node calling the method.
|
109
|
+
foo.originating
|
110
|
+
bar.originating?(baz)
|
111
|
+
|
112
|
+
What nodes can I reach from this one?
|
113
|
+
Gives us an array of nodes that are connected to this one in the outward (to) direction.
|
114
|
+
Includes the node calling the method.
|
115
|
+
bar.reachable
|
116
|
+
foo.reachable?(baz)
|
117
|
+
|
118
|
+
Path Finding:
|
119
|
+
foo.shortest_path_to(baz)
|
120
|
+
returns [foo, bar,baz]
|
121
|
+
|
122
|
+
All these methods return the node object with an additional weight field.
|
123
|
+
This enables you to query the weights associated with the edges found.
|
124
|
+
|
125
|
+
Behind the Scenes:
|
126
|
+
When you declare acts_as_oqgraph then the edge class gets created. You can add extra functionality
|
127
|
+
if you wish to the edge class by the usual Ruby monkey patching methods.
|
128
|
+
|
129
|
+
The OQGraph table will also get created if it does not exist. The OQGraph table is volatile, it holds data in
|
130
|
+
memory only. The table structure is not volatile and gets stored in the db.
|
131
|
+
When your application starts up it will put all the edges into the graph table and update them as
|
132
|
+
they are created, deleted or modified. This could slow things down at startup but caching classes in production
|
133
|
+
means it does not happen on every request. I will add functionality to only update the oqgraph table if the
|
134
|
+
db server has been stopped, once I figure out the best way to do it.
|
135
|
+
|
136
|
+
|
137
|
+
Copyright (c) 2010 Stuart Coyle, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "acts_as_oqgraph"
|
8
|
+
gem.summary = %Q{Use the Open Query Graph engine with Active Record}
|
9
|
+
gem.description = %Q{Acts As OQGraph allows ActiveRecord models to use the fast ans powerful OQGraph engine for MYSQL.}
|
10
|
+
gem.email = "stuart.coyle@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/stuart/acts_as_oqgraph"
|
12
|
+
gem.authors = ["Stuart Coyle"]
|
13
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/test_*.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'rcov/rcovtask'
|
29
|
+
Rcov::RcovTask.new do |test|
|
30
|
+
test.libs << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
rescue LoadError
|
35
|
+
task :rcov do
|
36
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
task :test => :check_dependencies
|
41
|
+
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
47
|
+
|
48
|
+
rdoc.rdoc_dir = 'rdoc'
|
49
|
+
rdoc.title = "testy #{version}"
|
50
|
+
rdoc.rdoc_files.include('README*')
|
51
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
52
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require File.join(File.dirname(__FILE__),'graph_edge')
|
3
|
+
require 'mysql'
|
4
|
+
|
5
|
+
module OQGraph
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Usage:
|
12
|
+
#
|
13
|
+
# Class Foo < ActiveRecord::Base
|
14
|
+
# acts_as_oqgraph +options+
|
15
|
+
# ....
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Options:
|
19
|
+
# :class_name - The name of the edge class, defaults to current class name appended with Edge. eg FooEdge
|
20
|
+
# :table_name - The name of the edge table, defaults to table name of the specified class, eg foo_edges
|
21
|
+
# :oqgraph_table_name - the name of the volatile oqgraph table. Default foo_edge_oqgraph
|
22
|
+
# :from_key - The from key field in the edge table. Default 'from_id'
|
23
|
+
# :to_key - The to key field in the edge table. Default: 'to_id'
|
24
|
+
# :weight_column - The weight field in the edge table.
|
25
|
+
#
|
26
|
+
# Setup:
|
27
|
+
# This gem requires the use of MySQL or MariaDB with the OQGraph engine plugin.
|
28
|
+
# For details of this see: http://openquery.com/products/graph-engine
|
29
|
+
#
|
30
|
+
# You will need a table for the edges with the following schema:
|
31
|
+
# create_table foo_edges do |t|
|
32
|
+
# t.integer from_id
|
33
|
+
# t.integer to_id
|
34
|
+
# t.double weight
|
35
|
+
# end
|
36
|
+
# The field names and table name can be changed via the options listed above.
|
37
|
+
#
|
38
|
+
# The gem will automatically create the oqgraph table.
|
39
|
+
#
|
40
|
+
# Examples of use:
|
41
|
+
#
|
42
|
+
# Creating and removing edges:
|
43
|
+
# foo.create_edge_to(bar)
|
44
|
+
# bar.create_edge_to(baz, 2.0)
|
45
|
+
# foo.remove_edge_to(bar) : Note that this removes ALL edges to bar from foo.
|
46
|
+
# or alternatively:
|
47
|
+
# foo.outgoing_nodes << bar
|
48
|
+
# foo.outgoing_nodes
|
49
|
+
#
|
50
|
+
# Examining edges:
|
51
|
+
# foo.originating
|
52
|
+
# returns [foo]
|
53
|
+
# baz.originating
|
54
|
+
# returns [foo, bar,baz]
|
55
|
+
# bar.reachable
|
56
|
+
# returns [bar, baz]
|
57
|
+
# foo.reachable?(baz)
|
58
|
+
# returns true
|
59
|
+
#
|
60
|
+
# Path Finding:
|
61
|
+
# foo.shortest_path_to(baz)
|
62
|
+
# returns [foo, bar,baz]
|
63
|
+
#
|
64
|
+
# All these methods return the node object with an additional weight field.
|
65
|
+
# This enables you to query the weights associated with the edges found.
|
66
|
+
#
|
67
|
+
def acts_as_oqgraph(options = {})
|
68
|
+
|
69
|
+
unless check_for_oqgraph_engine
|
70
|
+
raise "acts_as_oqgraph requires the OQGRAPH engine. Install the oqgraph plugin with the following SQL: INSTALL PLUGIN oqgraph SONAME 'oqgraph_engine.so'"
|
71
|
+
end
|
72
|
+
|
73
|
+
class_name = options[:class_name] || "#{self.name}Edge"
|
74
|
+
edge_table_name = options[:table_name] || class_name.pluralize.underscore
|
75
|
+
oqgraph_table_name = options[:oqgraph_table_name] || "#{self.name}Oqgraph".underscore
|
76
|
+
from_key = options[:from_key] || 'from_id'
|
77
|
+
to_key = options[:to_key] || 'to_id'
|
78
|
+
|
79
|
+
# Create the Edge Model
|
80
|
+
eval <<-EOS
|
81
|
+
class ::#{class_name} < ::GraphEdge
|
82
|
+
set_table_name "#{edge_table_name}"
|
83
|
+
|
84
|
+
belongs_to :from, :class_name => '#{self.name}', :foreign_key => '#{from_key}'
|
85
|
+
belongs_to :to, :class_name => '#{self.name}', :foreign_key => '#{to_key}'
|
86
|
+
|
87
|
+
@@oqgraph_table_name = '#{oqgraph_table_name}'
|
88
|
+
@@from_key = '#{from_key}'
|
89
|
+
@@to_key = '#{to_key}'
|
90
|
+
@@node_class = #{self}
|
91
|
+
|
92
|
+
create_graph_table
|
93
|
+
end
|
94
|
+
EOS
|
95
|
+
|
96
|
+
has_many :outgoing_edges, {
|
97
|
+
:class_name => class_name,
|
98
|
+
:foreign_key => from_key,
|
99
|
+
:include => :to,
|
100
|
+
:dependent => :destroy
|
101
|
+
}
|
102
|
+
|
103
|
+
has_many :incoming_edges, {
|
104
|
+
:class_name => class_name,
|
105
|
+
:foreign_key => to_key,
|
106
|
+
:include => :from,
|
107
|
+
:dependent => :destroy
|
108
|
+
}
|
109
|
+
|
110
|
+
has_many :outgoing_nodes, :through => :outgoing_edges, :source => :to
|
111
|
+
has_many :incoming_nodes, :through => :incoming_edges, :source => :from
|
112
|
+
|
113
|
+
class_eval <<-EOF
|
114
|
+
include OQGraph::InstanceMethods
|
115
|
+
|
116
|
+
def self.edge_class
|
117
|
+
#{class_name.classify}
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.edge_table
|
121
|
+
'#{edge_table_name}'
|
122
|
+
end
|
123
|
+
|
124
|
+
EOF
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Check that we have the OQGraph engine plugin installed in MySQL
|
130
|
+
def check_for_oqgraph_engine
|
131
|
+
begin
|
132
|
+
result = false
|
133
|
+
engines = self.connection.execute("SHOW ENGINES")
|
134
|
+
engines.each_hash do |engine|
|
135
|
+
result = true if (engine["Engine"]=="OQGRAPH" and engine["Support"]=="YES")
|
136
|
+
end
|
137
|
+
return result
|
138
|
+
rescue ActiveRecord::StatementInvalid => e
|
139
|
+
# This will occur if the table is not using MySQL
|
140
|
+
# TODO: Raise a sensible error message here.
|
141
|
+
return false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
module InstanceMethods
|
147
|
+
|
148
|
+
# The class used for the edges between nodes
|
149
|
+
def edge_class
|
150
|
+
self.class.edge_class
|
151
|
+
end
|
152
|
+
|
153
|
+
# Creates a one way edge from this node to another with a weight.
|
154
|
+
def create_edge_to(other, weight = 1.0)
|
155
|
+
edge_class.create!(:from_id => id, :to_id => other.id, :weight => weight)
|
156
|
+
end
|
157
|
+
|
158
|
+
# +other+ graph node to edge to
|
159
|
+
# +weight+ positive float denoting edge weight
|
160
|
+
# Creates a two way edge between this node and another.
|
161
|
+
def create_edge_to_and_from(other, weight = 1.0)
|
162
|
+
edge_class.create!(:from_id => id, :to_id => other.id, :weight => weight)
|
163
|
+
edge_class.create!(:from_id => other.id, :to_id => id, :weight => weight)
|
164
|
+
end
|
165
|
+
|
166
|
+
# +other+ The target node to find a route to
|
167
|
+
# +options+ A hash of options: Currently the only option is
|
168
|
+
# :method => :djiskstra or :breadth_first
|
169
|
+
# Returns an array of nodes in order starting with this node and ending in the target
|
170
|
+
# This will be the shortest path from this node to the other.
|
171
|
+
# The :djikstra method takes edge weights into account, the :breadth_first does not.
|
172
|
+
def shortest_path_to(other, options = {:method => :djikstra})
|
173
|
+
edge_class.shortest_path(self,other, options)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns an array of all nodes which can trace to this node
|
177
|
+
def originating
|
178
|
+
edge_class.originating_vertices(self)
|
179
|
+
end
|
180
|
+
|
181
|
+
# true if the other node can reach this node.
|
182
|
+
def originating?(other)
|
183
|
+
originating.include?(other)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Returns all nodes reachable from this node.
|
187
|
+
def reachable
|
188
|
+
edge_class.reachable_vertices(self)
|
189
|
+
end
|
190
|
+
|
191
|
+
# true if the other node is reachable from this one
|
192
|
+
def reachable?(other)
|
193
|
+
reachable.include?(other)
|
194
|
+
end
|
195
|
+
|
196
|
+
# +other+ The target node to find a route to
|
197
|
+
# Gives the path weight as a float of the shortest path to the other
|
198
|
+
def path_weight_to(other)
|
199
|
+
edge_class.shortest_path(self,other,:method => :djikstra).map{|edge| edge.weight.to_f}.sum
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
ActiveRecord::Base.class_eval { include OQGraph }
|
data/lib/graph_edge.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# This is the non-volatile store for the graph data.
|
2
|
+
|
3
|
+
class GraphEdge < ActiveRecord::Base
|
4
|
+
|
5
|
+
after_create :add_to_graph
|
6
|
+
after_destroy :remove_from_graph
|
7
|
+
after_update :update_graph
|
8
|
+
|
9
|
+
cattr_accessor :node_class, :oqgraph_table_name, :to_key, :from_key
|
10
|
+
|
11
|
+
# Creates the OQgraph table if it does not exist.
|
12
|
+
# Deletes all entries if it does exist and then repopulates with
|
13
|
+
# current edges. TODO Optimise this so that it only does so if the
|
14
|
+
# DB server has been restarted.
|
15
|
+
def self.create_graph_table
|
16
|
+
connection.execute <<-EOS
|
17
|
+
CREATE TABLE IF NOT EXISTS #{oqgraph_table_name} (
|
18
|
+
latch SMALLINT UNSIGNED NULL,
|
19
|
+
origid BIGINT UNSIGNED NULL,
|
20
|
+
destid BIGINT UNSIGNED NULL,
|
21
|
+
weight DOUBLE NULL,
|
22
|
+
seq BIGINT UNSIGNED NULL,
|
23
|
+
linkid BIGINT UNSIGNED NULL,
|
24
|
+
KEY (latch, origid, destid) USING HASH,
|
25
|
+
KEY (latch, destid, origid) USING HASH
|
26
|
+
) ENGINE=OQGRAPH;
|
27
|
+
EOS
|
28
|
+
|
29
|
+
connection.execute("DELETE FROM #{oqgraph_table_name}")
|
30
|
+
|
31
|
+
self.all.each do |edge|
|
32
|
+
edge.add_to_graph
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.shortest_path(from, to, options)
|
37
|
+
latch = case options[:method]
|
38
|
+
when :breadth_first then 2
|
39
|
+
else 1
|
40
|
+
end
|
41
|
+
latch = 1
|
42
|
+
latch = 2 if options[:method] == :breadth_first
|
43
|
+
|
44
|
+
sql = <<-EOS
|
45
|
+
WHERE latch = #{latch} AND origid = #{from.id} AND destid = #{to.id}
|
46
|
+
ORDER BY seq;
|
47
|
+
EOS
|
48
|
+
|
49
|
+
node_class.find_by_sql select_for_node << sql
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.originating_vertices(to)
|
53
|
+
sql = <<-EOS
|
54
|
+
WHERE latch = 1 AND destid = #{to.id}
|
55
|
+
ORDER BY seq;
|
56
|
+
EOS
|
57
|
+
|
58
|
+
node_class.find_by_sql select_for_node << sql
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.reachable_vertices(from)
|
62
|
+
sql = <<-EOS
|
63
|
+
WHERE latch = 1 AND origid = #{from.id}
|
64
|
+
ORDER BY seq;
|
65
|
+
EOS
|
66
|
+
|
67
|
+
node_class.find_by_sql select_for_node << sql
|
68
|
+
end
|
69
|
+
|
70
|
+
# FIXME: Note this currently does not work.
|
71
|
+
# I suspect a bug in OQGRaph engine.
|
72
|
+
def self.in_edges(to)
|
73
|
+
sql = <<-EOS
|
74
|
+
WHERE latch = 0 AND destid = #{to.id}
|
75
|
+
EOS
|
76
|
+
|
77
|
+
node_class.find_by_sql select_for_node << sql
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.out_edges(from)
|
81
|
+
sql = <<-EOS
|
82
|
+
WHERE latch = 0 AND origid = #{from.id}
|
83
|
+
EOS
|
84
|
+
|
85
|
+
node_class.find_by_sql select_for_node << sql
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def add_to_graph
|
90
|
+
connection.execute <<-EOS
|
91
|
+
INSERT INTO #{oqgraph_table_name} (origid, destid, weight)
|
92
|
+
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{weight || 1.0});
|
93
|
+
EOS
|
94
|
+
end
|
95
|
+
|
96
|
+
def remove_from_graph
|
97
|
+
connection.execute <<-EOS
|
98
|
+
DELETE FROM #{oqgraph_table_name} WHERE origid = #{self.send(self.class.from_key)} AND destid = #{self.send(self.class.to_key)};
|
99
|
+
EOS
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_graph
|
103
|
+
connection.execute <<-EOS
|
104
|
+
UPDATE #{oqgraph_table_name}
|
105
|
+
SET origid = #{self.send(self.class.from_key)},
|
106
|
+
destid = #{self.send(self.class.to_key)},
|
107
|
+
weight = #{weight}
|
108
|
+
WHERE origid = #{self.send(self.class.from_key + '_was')} AND destid = #{self.send(self.class.to_key + '_was')};
|
109
|
+
EOS
|
110
|
+
end
|
111
|
+
def self.select_for_node
|
112
|
+
sql = "SELECT "
|
113
|
+
sql << node_class.columns.map{|column| "#{node_table}.#{column.name}"}.join(",")
|
114
|
+
sql << ", #{oqgraph_table_name}.weight FROM #{oqgraph_table_name} JOIN #{node_table} ON (linkid=id) "
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.node_table
|
118
|
+
node_class.table_name
|
119
|
+
end
|
120
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'mysql'
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/test_case'
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
9
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
10
|
+
require 'acts_as_oqgraph'
|
11
|
+
|
12
|
+
class ActiveSupport::TestCase
|
13
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestActsAsOqgraph < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection(
|
7
|
+
:adapter => "mysql",
|
8
|
+
:host => "localhost",
|
9
|
+
:username => "root",
|
10
|
+
:password => "",
|
11
|
+
:database => "test"
|
12
|
+
)
|
13
|
+
|
14
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_models(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) );")
|
15
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_model_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, from_id INTEGER, to_id INTEGER, weight DOUBLE);")
|
16
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS custom_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, from_id INTEGER, to_id INTEGER, weight DOUBLE);")
|
17
|
+
|
18
|
+
require File.join(File.dirname(__FILE__),'models/custom_test_model')
|
19
|
+
require File.join(File.dirname(__FILE__),'models/test_model')
|
20
|
+
|
21
|
+
@test_1 = TestModel.create(:name => 'a')
|
22
|
+
@test_2 = TestModel.create(:name => 'b')
|
23
|
+
@test_3 = TestModel.create(:name => 'c')
|
24
|
+
@test_4 = TestModel.create(:name => 'd')
|
25
|
+
@test_5 = TestModel.create(:name => 'e')
|
26
|
+
@test_6 = TestModel.create(:name => 'f')
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def teardown
|
31
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_models;")
|
32
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_model_edges;")
|
33
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS custom_edges;")
|
34
|
+
ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph;")
|
35
|
+
ActiveRecord::Base.connection.execute("DELETE FROM custom_test_model_oqgraph;")
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_edge_table_and_class_names
|
40
|
+
assert_equal "test_model_edges", TestModel.edge_table
|
41
|
+
assert_equal TestModelEdge, TestModel.edge_class
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_edge_class_created
|
45
|
+
assert_nothing_raised do
|
46
|
+
::TestModelEdge
|
47
|
+
::CustomEdge
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_creation_of_oqgraph_and_edge_tables
|
52
|
+
mysql = Mysql.new('localhost', 'root', '', 'test')
|
53
|
+
assert mysql.list_tables.include?('test_model_oqgraph')
|
54
|
+
assert mysql.list_tables.include?('test_model_edges')
|
55
|
+
fields = mysql.list_fields('test_model_oqgraph').fetch_fields.map{|f| f.name}
|
56
|
+
assert fields.include?('origid')
|
57
|
+
assert fields.include?('destid')
|
58
|
+
assert fields.include?('weight')
|
59
|
+
assert fields.include?('latch')
|
60
|
+
assert fields.include?('seq')
|
61
|
+
assert fields.include?('linkid')
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_edge_class_name_option
|
65
|
+
assert_equal 'custom_edges', CustomTestModel.edge_table
|
66
|
+
assert_equal CustomEdge, CustomTestModel.edge_class
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_test_model_edge_creation
|
70
|
+
@test_1.create_edge_to(@test_2)
|
71
|
+
assert_not_nil edge = TestModelEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id, :weight => 1.0})
|
72
|
+
assert @test_1.outgoing_nodes.include?(@test_2)
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_edge_creation_through_association
|
76
|
+
@test_1.outgoing_nodes << @test_2
|
77
|
+
assert_not_nil edge = TestModelEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id})
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_test_model_undirected_edge_creation
|
81
|
+
@test_1.create_edge_to_and_from(@test_2)
|
82
|
+
assert_not_nil edge = TestModelEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id, :weight => 1.0})
|
83
|
+
assert_not_nil edge = TestModelEdge.find(:first, :conditions => {:from_id => @test_2.id, :to_id => @test_1.id, :weight => 1.0})
|
84
|
+
assert @test_1.outgoing_nodes.include?(@test_2)
|
85
|
+
assert @test_1.incoming_nodes.include?(@test_2)
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_adding_weight_to_edges
|
89
|
+
@test_1.create_edge_to(@test_2, 2.0)
|
90
|
+
assert_not_nil edge = TestModelEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id})
|
91
|
+
assert edge.weight = 2.0
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_edge_model_creation_creates_oqgraph_edge
|
95
|
+
@test_1.create_edge_to(@test_2, 2.5)
|
96
|
+
oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_model_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
|
97
|
+
assert_equal [nil,"1","2","2.5",nil,nil], oqedge.fetch_row
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_edge_model_removal_deletes_oqgraph_edge
|
101
|
+
@test_1.outgoing_nodes << @test_2
|
102
|
+
edge = @test_1.outgoing_edges.find(:first, :conditions => {:to_id => @test_2.id})
|
103
|
+
edge.destroy
|
104
|
+
oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_model_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
|
105
|
+
assert_equal nil, oqedge.fetch_row
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_edge_model_update
|
109
|
+
edge = @test_1.create_edge_to(@test_2, 2.5)
|
110
|
+
edge.update_attributes(:weight => 3.0)
|
111
|
+
oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_model_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
|
112
|
+
assert_equal [nil,"1","2","3",nil,nil], oqedge.fetch_row
|
113
|
+
edge.update_attributes(:to_id => 3)
|
114
|
+
oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_model_oqgraph WHERE origid=#{@test_1.id} AND destid=3;")
|
115
|
+
assert_equal [nil,"1","3","3",nil,nil], oqedge.fetch_row
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_gettting_the_shortest_path
|
119
|
+
# a -- b -- c -- d
|
120
|
+
@test_1.create_edge_to @test_2
|
121
|
+
@test_2.create_edge_to @test_3
|
122
|
+
@test_3.create_edge_to @test_4
|
123
|
+
assert_equal [@test_1, @test_2, @test_3, @test_4], @test_1.shortest_path_to(@test_4)
|
124
|
+
assert_equal ['a','b','c','d'], @test_1.shortest_path_to(@test_4).map(&:name)
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_getting_shortest_path_more_complex
|
128
|
+
#
|
129
|
+
# a -- b -- c -- d
|
130
|
+
# | /
|
131
|
+
# e-- f
|
132
|
+
@test_1.create_edge_to @test_2
|
133
|
+
@test_2.create_edge_to @test_3
|
134
|
+
@test_3.create_edge_to @test_4
|
135
|
+
@test_2.create_edge_to @test_5
|
136
|
+
@test_5.create_edge_to @test_6
|
137
|
+
@test_4.create_edge_to @test_6
|
138
|
+
assert_equal [@test_1, @test_2, @test_5, @test_6], @test_1.shortest_path_to(@test_6)
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_path_returns_weight
|
142
|
+
# a -- b -- c -- d
|
143
|
+
@test_1.create_edge_to @test_2, 2.0
|
144
|
+
@test_2.create_edge_to @test_3, 1.5
|
145
|
+
@test_3.create_edge_to @test_4, 1.2
|
146
|
+
assert_equal [nil,"2","1.5","1.2"], @test_1.shortest_path_to(@test_4).map(&:weight)
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_path_weight
|
150
|
+
# a -- b -- c -- d
|
151
|
+
@test_1.create_edge_to @test_2, 2.0
|
152
|
+
@test_2.create_edge_to @test_3, 1.5
|
153
|
+
@test_3.create_edge_to @test_4, 1.2
|
154
|
+
assert_equal 4.7, @test_1.path_weight_to(@test_4)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_path_find_breadth_first
|
158
|
+
@test_1.outgoing_nodes << @test_2
|
159
|
+
@test_2.outgoing_nodes << @test_3
|
160
|
+
@test_3.outgoing_nodes << @test_4
|
161
|
+
assert_equal [@test_1, @test_2, @test_3, @test_4], @test_1.shortest_path_to(@test_4, :method => :breadth_first)
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_get_originating_nodes
|
165
|
+
@test_1.create_edge_to @test_2
|
166
|
+
@test_2.create_edge_to @test_3
|
167
|
+
assert_equal [@test_2, @test_1] , @test_2.originating
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_get_reachable_nodes
|
171
|
+
@test_1.create_edge_to @test_2
|
172
|
+
@test_2.create_edge_to @test_3
|
173
|
+
assert_equal [@test_2, @test_3] , @test_2.reachable
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_get_originating?
|
177
|
+
@test_1.create_edge_to @test_2
|
178
|
+
@test_2.create_edge_to @test_3
|
179
|
+
assert @test_2.originating?(@test_1)
|
180
|
+
assert !@test_2.originating?(@test_3)
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_get_incoming_nodes
|
184
|
+
@test_1.create_edge_to @test_2
|
185
|
+
@test_2.create_edge_to @test_3
|
186
|
+
assert_equal [@test_1] , @test_2.incoming_nodes
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_get_outgoing_nodes
|
190
|
+
@test_1.create_edge_to @test_2
|
191
|
+
@test_2.create_edge_to @test_3
|
192
|
+
assert_equal [@test_3] , @test_2.outgoing_nodes
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: acts_as_oqgraph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Stuart Coyle
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-21 00:00:00 +10:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Acts As OQGraph allows ActiveRecord models to use the fast ans powerful OQGraph engine for MYSQL.
|
22
|
+
email: stuart.coyle@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- .document
|
32
|
+
- .gitignore
|
33
|
+
- LICENSE
|
34
|
+
- README.rdoc
|
35
|
+
- Rakefile
|
36
|
+
- VERSION
|
37
|
+
- lib/acts_as_oqgraph.rb
|
38
|
+
- lib/graph_edge.rb
|
39
|
+
- test/helper.rb
|
40
|
+
- test/models/custom_test_model.rb
|
41
|
+
- test/models/test_model.rb
|
42
|
+
- test/test_acts_as_oqgraph.rb
|
43
|
+
has_rdoc: true
|
44
|
+
homepage: http://github.com/stuart/acts_as_oqgraph
|
45
|
+
licenses: []
|
46
|
+
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --charset=UTF-8
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.6
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Use the Open Query Graph engine with Active Record
|
73
|
+
test_files:
|
74
|
+
- test/helper.rb
|
75
|
+
- test/models/custom_test_model.rb
|
76
|
+
- test/models/test_model.rb
|
77
|
+
- test/test_acts_as_oqgraph.rb
|