acts_as_oqgraph 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/acts_as_oqgraph.gemspec +1 -1
- data/lib/acts_as_oqgraph.rb +7 -6
- data/lib/graph_edge.rb +57 -40
- data/test/test_acts_as_oqgraph.rb +8 -5
- metadata +2 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.6
|
data/acts_as_oqgraph.gemspec
CHANGED
data/lib/acts_as_oqgraph.rb
CHANGED
@@ -60,7 +60,10 @@ module OQGraph
|
|
60
60
|
# Path Finding:
|
61
61
|
# foo.shortest_path_to(baz)
|
62
62
|
# returns [foo, bar,baz]
|
63
|
-
#
|
63
|
+
#
|
64
|
+
# With breadth first edge weights are not taken into account:
|
65
|
+
# foo.shortest_path_to(baz, :method => :breadth_first)
|
66
|
+
#
|
64
67
|
# All these methods return the node object with an additional weight field.
|
65
68
|
# This enables you to query the weights associated with the edges found.
|
66
69
|
#
|
@@ -139,9 +142,7 @@ module OQGraph
|
|
139
142
|
end
|
140
143
|
return result
|
141
144
|
rescue ActiveRecord::StatementInvalid => e
|
142
|
-
|
143
|
-
# TODO: Raise a sensible error message here.
|
144
|
-
return false
|
145
|
+
raise "MySQL or MariaDB 5.1 or above with the OQGRAPH engine is required for the acts_as_oqgraph gem.\nThe following error was raised: #{e.inspect}"
|
145
146
|
end
|
146
147
|
end
|
147
148
|
end
|
@@ -178,7 +179,7 @@ module OQGraph
|
|
178
179
|
|
179
180
|
# Returns an array of all nodes which can trace to this node
|
180
181
|
def originating
|
181
|
-
edge_class.
|
182
|
+
edge_class.originating_nodes(self)
|
182
183
|
end
|
183
184
|
|
184
185
|
# true if the other node can reach this node.
|
@@ -188,7 +189,7 @@ module OQGraph
|
|
188
189
|
|
189
190
|
# Returns all nodes reachable from this node.
|
190
191
|
def reachable
|
191
|
-
edge_class.
|
192
|
+
edge_class.reachable_nodes(self)
|
192
193
|
end
|
193
194
|
|
194
195
|
# true if the other node is reachable from this one
|
data/lib/graph_edge.rb
CHANGED
@@ -6,12 +6,13 @@ class GraphEdge < ActiveRecord::Base
|
|
6
6
|
after_destroy :remove_from_graph
|
7
7
|
after_update :update_graph
|
8
8
|
|
9
|
-
|
10
9
|
# Creates the OQgraph table if it does not exist.
|
11
10
|
# Deletes all entries if it does exist and then repopulates with
|
12
|
-
# current edges. TODO Optimise this so that it only does so if the
|
13
|
-
# DB server has been restarted.
|
11
|
+
# current edges. TODO: Optimise this so that it only does so if the
|
12
|
+
# DB server has been restarted rather than when the app is restarted.
|
14
13
|
def self.create_graph_table
|
14
|
+
connection.execute "DROP TABLE IF EXISTS #{oqgraph_table_name}"
|
15
|
+
|
15
16
|
connection.execute <<-EOS
|
16
17
|
CREATE TABLE IF NOT EXISTS #{oqgraph_table_name} (
|
17
18
|
latch SMALLINT UNSIGNED NULL,
|
@@ -25,13 +26,17 @@ class GraphEdge < ActiveRecord::Base
|
|
25
26
|
) ENGINE=OQGRAPH;
|
26
27
|
EOS
|
27
28
|
|
28
|
-
connection.execute
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
29
|
+
connection.execute <<-EOS
|
30
|
+
INSERT INTO #{oqgraph_table_name} (origid, destid, weight)
|
31
|
+
SELECT #{from_key}, #{to_key}, #{weight_column} FROM #{table_name}
|
32
|
+
EOS
|
33
33
|
end
|
34
34
|
|
35
|
+
# Returns the shortest path from node to node.
|
36
|
+
# +options+ A hash containing options.
|
37
|
+
# The only option is :method which can be
|
38
|
+
# :breadth_first to do a breadth first search.
|
39
|
+
# It defaults to using Djikstra's algorithm.
|
35
40
|
def self.shortest_path(from, to, options)
|
36
41
|
latch = case options[:method]
|
37
42
|
when :breadth_first then 2
|
@@ -48,73 +53,85 @@ class GraphEdge < ActiveRecord::Base
|
|
48
53
|
node_class.find_by_sql select_for_node << sql
|
49
54
|
end
|
50
55
|
|
51
|
-
|
56
|
+
# Finds all the nodes that lead to the node
|
57
|
+
def self.originating_nodes(node)
|
52
58
|
sql = <<-EOS
|
53
|
-
WHERE latch = 1 AND destid = #{
|
59
|
+
WHERE latch = 1 AND destid = #{node.id}
|
54
60
|
ORDER BY seq;
|
55
61
|
EOS
|
56
62
|
|
57
63
|
node_class.find_by_sql select_for_node << sql
|
58
64
|
end
|
59
65
|
|
60
|
-
|
66
|
+
# Finds all the nodes that are reachable from the node
|
67
|
+
def self.reachable_nodes(node)
|
61
68
|
sql = <<-EOS
|
62
|
-
WHERE latch = 1 AND origid = #{
|
69
|
+
WHERE latch = 1 AND origid = #{node.id}
|
63
70
|
ORDER BY seq;
|
64
71
|
EOS
|
65
72
|
|
66
73
|
node_class.find_by_sql select_for_node << sql
|
67
74
|
end
|
68
75
|
|
76
|
+
# Finds the edges leading directly into the node
|
69
77
|
# FIXME: Note this currently does not work.
|
70
78
|
# I suspect a bug in OQGRaph engine.
|
71
|
-
|
79
|
+
# Using the node classes incoming_nodes is equivalent to this.
|
80
|
+
def self.in_edges(node)
|
72
81
|
sql = <<-EOS
|
73
|
-
WHERE latch = 0 AND destid = #{
|
82
|
+
WHERE latch = 0 AND destid = #{node.id}
|
74
83
|
EOS
|
75
84
|
|
76
85
|
node_class.find_by_sql select_for_node << sql
|
77
86
|
end
|
78
87
|
|
79
|
-
|
88
|
+
# Finds the edges leading directly out of the node
|
89
|
+
def self.out_edges(node)
|
80
90
|
sql = <<-EOS
|
81
|
-
WHERE latch = 0 AND origid = #{
|
91
|
+
WHERE latch = 0 AND origid = #{node.id}
|
82
92
|
EOS
|
83
93
|
|
84
94
|
node_class.find_by_sql select_for_node << sql
|
85
95
|
end
|
86
|
-
|
87
|
-
def add_to_graph
|
88
|
-
connection.execute <<-EOS
|
89
|
-
REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
|
90
|
-
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{weight || 1.0})
|
91
|
-
EOS
|
92
|
-
end
|
93
96
|
|
94
97
|
private
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
WHERE origid = #{self.send(self.class.from_key
|
110
|
-
|
111
|
-
|
98
|
+
|
99
|
+
# Callback to add new graph edges to the OQGraph table
|
100
|
+
def add_to_graph
|
101
|
+
connection.execute <<-EOS
|
102
|
+
REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
|
103
|
+
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{self.send(self.class.weight_column) || 1.0})
|
104
|
+
EOS
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# Callback to remove deleted graph edges from the OQGraph table
|
109
|
+
def remove_from_graph
|
110
|
+
# Ignores trying to delete nonexistent records
|
111
|
+
connection.execute <<-EOS
|
112
|
+
DELETE IGNORE FROM #{oqgraph_table_name} WHERE origid = #{self.send(self.class.from_key)} AND destid = #{self.send(self.class.to_key)};
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
# Callback to update graph edges in the OQGraph table
|
117
|
+
def update_graph
|
118
|
+
connection.execute <<-EOS
|
119
|
+
UPDATE #{oqgraph_table_name}
|
120
|
+
SET origid = #{self.send(self.class.from_key)},
|
121
|
+
destid = #{self.send(self.class.to_key)},
|
122
|
+
weight = #{self.send(self.class.weight_column)}
|
123
|
+
WHERE origid = #{self.send(self.class.from_key + '_was')} AND destid = #{self.send(self.class.to_key + '_was')};
|
124
|
+
EOS
|
125
|
+
end
|
126
|
+
|
127
|
+
# nodoc
|
112
128
|
def self.select_for_node
|
113
129
|
sql = "SELECT "
|
114
130
|
sql << node_class.columns.map{|column| "#{node_table}.#{column.name}"}.join(",")
|
115
131
|
sql << ", #{oqgraph_table_name}.weight FROM #{oqgraph_table_name} JOIN #{node_table} ON (linkid=id) "
|
116
132
|
end
|
117
133
|
|
134
|
+
# Returns the table containing the nodes for these edges.
|
118
135
|
def self.node_table
|
119
136
|
node_class.table_name
|
120
137
|
end
|
@@ -2,7 +2,6 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class TestActsAsOqgraph < ActiveSupport::TestCase
|
4
4
|
def setup
|
5
|
-
|
6
5
|
ActiveRecord::Base.establish_connection(
|
7
6
|
:adapter => "mysql",
|
8
7
|
:host => "localhost",
|
@@ -13,8 +12,9 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
13
12
|
|
14
13
|
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_models(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) );")
|
15
14
|
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,
|
15
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS custom_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, orig_id INTEGER, dest_id INTEGER, length DOUBLE);")
|
17
16
|
|
17
|
+
# These requires need the tables to be created.
|
18
18
|
require File.join(File.dirname(__FILE__),'models/custom_test_model')
|
19
19
|
require File.join(File.dirname(__FILE__),'models/test_model')
|
20
20
|
|
@@ -24,7 +24,6 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
24
24
|
@test_4 = TestModel.create(:name => 'd')
|
25
25
|
@test_5 = TestModel.create(:name => 'e')
|
26
26
|
@test_6 = TestModel.create(:name => 'f')
|
27
|
-
|
28
27
|
end
|
29
28
|
|
30
29
|
def teardown
|
@@ -33,7 +32,6 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
33
32
|
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS custom_edges;")
|
34
33
|
ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph;")
|
35
34
|
ActiveRecord::Base.connection.execute("DELETE FROM custom_test_model_oqgraph;")
|
36
|
-
|
37
35
|
end
|
38
36
|
|
39
37
|
def test_edge_table_and_class_names
|
@@ -70,6 +68,11 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
70
68
|
assert_equal 'dest_id', CustomEdge.to_key
|
71
69
|
assert_equal 'orig_id', CustomEdge.from_key
|
72
70
|
assert_equal 'length', CustomEdge.weight_column
|
71
|
+
mysql = Mysql.new('localhost', 'root', '', 'test')
|
72
|
+
fields = mysql.list_fields('custom_edges').fetch_fields.map{|f| f.name}
|
73
|
+
assert fields.include?('orig_id')
|
74
|
+
assert fields.include?('dest_id')
|
75
|
+
assert fields.include?('length')
|
73
76
|
end
|
74
77
|
|
75
78
|
def test_test_model_edge_creation
|
@@ -222,7 +225,7 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
222
225
|
def test_deletion_of_nonexistent_edge_raises_error
|
223
226
|
edge = @test_1.create_edge_to @test_2
|
224
227
|
ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph WHERE destid = #{edge.to_id} AND origid = #{edge.from_id}")
|
225
|
-
|
228
|
+
assert_nothing_raised do
|
226
229
|
edge.destroy
|
227
230
|
end
|
228
231
|
end
|