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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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,3 @@
1
+ class CustomTestModel < ActiveRecord::Base
2
+ acts_as_oqgraph :class_name => 'CustomEdge', :from_key => 'orig_id', :to_key => 'dest_id'
3
+ end
@@ -0,0 +1,5 @@
1
+
2
+ class TestModel < ActiveRecord::Base
3
+ acts_as_oqgraph
4
+ end
5
+
@@ -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