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 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