acts_as_oqgraph 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/.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
|